-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
internal/resolver: introduce a new delegating resolver to handle both target URI and proxy address resolution #7857
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #7857 +/- ##
==========================================
- Coverage 82.21% 82.10% -0.12%
==========================================
Files 379 381 +2
Lines 38261 38432 +171
==========================================
+ Hits 31458 31555 +97
- Misses 5514 5576 +62
- Partials 1289 1301 +12
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some style and documentation comments on non test code in the first pass. Will review the test code for same next.
internal/internal.go
Outdated
// HTTPSProxyFromEnvironment returns the URL of the proxy to use | ||
// for testing purposes. It is used to override the `http.ProxyFromEnvironment` | ||
// function for testing purposes. | ||
HTTPSProxyFromEnvironment any //func(*http.Request) (*url.URL, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: space after slashes for consistency..
But also: why bother with making it an any
? It can just have the proper type. The reason for the any
s is to avoid circular dependencies, but the http and url packages in OSS will never import grpc/internal. :)
Or even, let's just move this directly into internal/resolver/delegatingresolver as an exported thing there since it won't need to be changed by any non-grpc code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked to keep it any
to limit the number of packages that internal
imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
External imports from internal
should be fine. Internal imports from internal
should be zero, or as close as possible.
Either way, I don't think this needs to be here at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// Populate returns a copy of addr with attributes containing the provided user | ||
// and connect address, which are needed during the CONNECT handshake for a | ||
// proxy connection. | ||
func Populate(addr resolver.Address, opts Options) resolver.Address { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: we use Set
and Get
for these same function names everywhere else; please rename for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a style guide recommendation against the use of the Get
prefix for getters. See: go/go-style/decisions#getters
We have a few options with alternative verbs for Set
. I like Extract
here since it makes sense:
- Rename
Populate
toSetOptions
and renameOption
toExtractOptions
- Rename
Populate
toSetConnectOptions
and renameOption
toExtractConnectOptions
and rename the structOptions
toConnectOptions
sinceOptions
is very generic even with the package name added to it, i.e.proxyattributes.Options
As a last option, we could get rid of the options struct altogether and go with
func SetConnectOptions(addr resolver.Address, user url.UserInfo, connectAddr string) resolver.Address) {}
func ExtractConnectOptions(addr resolver.Address) (user url.UserInfo, connectAddr string, ok bool) {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went with Populate to SetOptions and rename Option to ExtractOptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already use Get and Set here quite extensively, and I don't think would be considered a getter/setter, either.
grpc-go/internal/hierarchy/hierarchy.go
Line 64 in b3bdacb
func Get(addr resolver.Address) []string { |
grpc-go/internal/metadata/metadata.go
Line 61 in b3bdacb
func Get(addr resolver.Address) metadata.MD { |
If the name of the package is the thing you want to set/get in the attributes, then it works out very naturally in usages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Changed to Get
and Set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm clearly not reading things properly. Yes, Set
and Get
work great here since they would show up as proxyattributes.Get
and proxyattributes.Set
.
In the meantime, I also had a discussion with some folks on the readability team, trying to understand why they don't recommend the use of Get
prefix for getters. A few things to note from that conversation:
- Mostly getters and setters refer to methods on a type. So, package level functions that get and set something don't count here.
- There is no clear rationale for why a
Get
prefix is not recommended other than: https://go.dev/doc/effective_go#Getters (and the fact that this recommendation has been around for a long time and most existing code follows this pattern) - And the pattern suggested by @dfawley in this comment is also a recommended way of doing things for package level functions.
} | ||
|
||
func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) { | ||
if r.targetResolver != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider implementing a nopResolver
instead that just ignores all calls, so you don't need to spread != nil
checks throughout the code (and get a panic if you forget one).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But since delegating resolver is the default resolver in most cases, doesn't it make sense that if ClientConn calls ResolveNow
or Close
, we propagate the same to the child resolvers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant:
func New() {
r := delegatingResolver{}
if useTargetResolver { r.targetResolver = build() } else { r.targetResolver = nopResolver{} }
if useProxyResolver { r.proxyResolver = build() } else { r.proxyResolver = nopResolver{} }
Then you never need to check if these fields are nil, since they are always initialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/resolver/delegatingresolver/delegatingresolver_ext_test.go
Outdated
Show resolved
Hide resolved
} | ||
|
||
if r.targetResolver == nil { | ||
r.targetResolver = nopResolver{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just FYI, don't change this: An optimization for using such stateless no-op implementations is to have a single object of the struct that everyone uses. This saves allocating memory for multiple objects. Here it wouldn't help much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, empty structs are allocated specially, so if it's an empty struct it will not save memory to have a global instance of it. See this example:
https://go.dev/play/p/LmiMfy_XNP4
Running it (for me, right now) prints 0x574380 0x574380
-- that it matches shows there aren't any wasted allocations.
Not for the struct itself, at least. But if the struct is stored in an interface field, like it is here, then that interface has to be allocated to hold the struct's type + pointer. But this level of optimization also isn't important for us. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor style comment, otherwise LGTM!
|
||
var ( | ||
logger = grpclog.Component("delegating-resolver") | ||
//HTTPSProxyFromEnvironment will be overwritten in the tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Space missing after //
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
r.targetResolverState.Addresses = []resolver.Address{{Addr: target.Endpoint()}} | ||
r.targetResolverState.Endpoints = []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: target.Endpoint()}}}} | ||
r.targetResolverReady = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not actionable: This is essentially the same as using passthrough
as the target resolver. If we need to avoid sending the dial option to the delegating resolver, the channel could send passthrough
as the target resolver to get the same effect.
// mock the DNS resolution for the proxy resolver. It also registers the | ||
// original DNS resolver after the test is done. | ||
func setupDNS(t *testing.T) *manual.Resolver { | ||
mr := manual.NewBuilderWithScheme("dns") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call t.Helper()
at the top to mark this is a test helper for better error reporting.
https://google.github.io/styleguide/go/decisions#test-helpers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// the delegating resolver creates only a target resolver and that the | ||
// addresses returned by the delegating resolver exactly match those returned | ||
// by the target resolver. | ||
func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The package name is delegatingresolver_test
, we don't need to add DelegatingResolver
to all the test functions.
https://google.github.io/styleguide/go/decisions#package-vs-exported-symbol-name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore my previous comment. The test name can be used to filter tests in subdirectories, so having the TestDelegatingResolver
prefix is helpful.
delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe | ||
}() | ||
|
||
targetResolver := manual.NewBuilderWithScheme("dns") // Manual resolver to control the target resolution. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Move the code comments to newlines everywhere to be consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// createTestResolverClientConn initializes a [testutils.ResolverClientConn] and | ||
// returns it along with channels for resolver state updates and errors. | ||
func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { | ||
stateCh := make(chan resolver.State, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark this as a test helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
targetResolverReady bool // indicates if an update from the target resolver has been received | ||
proxyResolverReady bool // indicates if an update from the proxy resolver has been received |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider removing these and using nil
for targetResolverState
and nil
for proxyAddrs
to indicate that the state/addresses are "valid".
Having two different pieces of state that represent parts of the same thing should be avoided. I.e. what does it mean if "!targetResolverReady && len(targetResolverState.Endpoints) > 0
"? That's illegal. So you can couple the two things into one field and avoid such illegal state combinations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the condition as well as change the field targetResolverState
to a pointer *resolver.State
to facilitate differentiation in a no update and an empty update from the target resolver in the condition. Because as per the flow, we want load balancer to handle an empty update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM pending the two small changes requested.
// curState contains all necessary information when passed to UpdateState. | ||
// The state update is only sent after both the target and proxy resolvers | ||
// have sent their updates, and curState has been updated with the combined | ||
// addresses. | ||
curState := *r.targetResolverState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this down to where you commented about it.
@@ -259,12 +254,17 @@ func (r *delegatingResolver) updateProxyResolverState(state resolver.State) erro | |||
logger.Infof("Addresses received from proxy resolver: %s", state.Addresses) | |||
} | |||
if len(state.Endpoints) > 0 { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this blank line
As part of fixing #7556 , we create a new delegating resolver that that handles proxy configuration and delegates to child resolvers for target and proxy address resolution and does the following:
Coping the context verbatim form the issue:
Background :
Before
grpc.NewClient
was introduced,grpc.Dial
was the primary way to establish gRPC connections. Withgrpc.Dial
, the default "passthrough" resolver simply passed the target address unchanged to the transport layer. Whengrpc.NewClient
was introduced, it switched to the "dns" resolver by default, which resolves the target address to IP addresses before passing them to the transport. This change inadvertently altered how proxies are handled when configured via environment variables. This behavior is inconsistent with what we expect i.e resolution to happen on proxy.The complete proposal :
RELEASE NOTES: N/A