-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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
xds: generic xds client resource watching e2e #8183
base: master
Are you sure you want to change the base?
xds: generic xds client resource watching e2e #8183
Conversation
c99ca98
to
02db583
Compare
@dfawley could you do a pass on the implementation? For now, I have removed the metrics from client as it requires some more thought. It can be added in separate commit or PR. Otherwise the main implementation can be reviewed to make sure we are following the right approach and do the changes (if needed) before we go further. Also, I will be adding more e2e tests in subsequent commits |
aeb32c1
to
7064881
Compare
4a038d9
to
3bacb44
Compare
@dfawley i have made the change to use ServerConfig and ServerIdentifier as map key for xdsclient and transport respectively. Now we don't need equal. Only thing is for grpcTransport, we return runtime error if extensions are not Could you do the pass on last commit and confirm if we can finalize this approach? |
03adc89
to
5bcc906
Compare
Didn't we at some point say that we will move the code as-in in one PR and then make changes in a follow-up, so that we can clearly see what is changing. If that is not feasible, could you please describe the changes in detail in each of those non-test files that are being copied over. Thanks. |
xds/internal/clients/config.go
Outdated
@@ -59,6 +59,11 @@ type ServerIdentifier struct { | |||
// | |||
// For example, a custom TransportBuilder might use this field to | |||
// configure a specific security credentials. | |||
// | |||
// Extensions must be able to be used as a map key, or the client may |
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.
How about:
// Extensions may be any type that is comparable, as they are used as map keys internally.
// See: https://go.dev/ref/spec#Comparison_operators.
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.
Added. Thanks
xds/internal/clients/config.go
Outdated
@@ -59,6 +59,11 @@ type ServerIdentifier struct { | |||
// | |||
// For example, a custom TransportBuilder might use this field to | |||
// configure a specific security credentials. | |||
// | |||
// Extensions must be able to be used as a map key, or the client may | |||
// panic. Any equivalent extensions in all ServerIdentifiers present in a |
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.
Can you please help me understand this.
// Any equivalent extensions in all ServerIdentifiers present in a
// single client's configuration should have the same value. Not following
// this restriction may result in excess resource usage.
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.
this basically means if all the fields in ServerIdentifier are not equal between 2 server identifiers, then they will create a separate transport to the server. So, even if they are pointing to same server but some fields are different they will create two separate transports to that server.
|
||
func ExampleServerIdentifierExtension() { | ||
si := clients.ServerIdentifier{ServerURI: "localhost:5678", Extensions: grpctransport.ServerIdentifierExtension{Credentials: "local"}} | ||
fmt.Printf("%v", si) |
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 needs to a comment with the expected output. Otherwise, this does nothing.
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.
Oh yeah actually, you need to mention the output as a comment below. I added. https://go.dev/blog/examples
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.
Or you can not print it.
Can you docstring examples to explain what they're attempting to show?
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.
Added the docstring. Still printing though but combined print and initialization in single line.
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.
By examples, do you mean add more examples?
// It must be set by value (not pointer) in the | ||
// clients.ServerIdentifier.Extensions field (See Example). |
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.
Why? Pointer types are comparable too? Maybe I'm missing something.
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.
With pointer there are 2 issues
- two different pointer with same values are unequal
- same pointers with different values are equal
So, we decided to go by value
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.
same pointers with different values are equal
That's not possible. But (1) is the reason why using by value is preferred. Otherwise you have to reuse instances for equivalent values.
type Builder struct { | ||
// Credentials is a map of credentials names to credentials.Bundle which | ||
// can be used to connect to the server. | ||
Credentials map[string]credentials.Bundle |
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.
Does this have to exported?
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.
No. Unexported
|
||
if logger.V(2) { | ||
logger.Info("Creating a new connection to the server for ServerIdentifier: %v", si) |
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: s/connection/transport
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.
Also, you could move this log to be right before the last return statement, and change "Creating" to "Created".
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
t.Fatalf("Got nil `tr1` transport from Build(serverID1), want non-nil") | ||
} | ||
if tr1.(*grpcTransport).cc == nil { | ||
t.Fatalf("Got nil grpc.ClientConn in transport `tr1`, want non-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.
Is it ever possible for this to be 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.
I actually removed these stuff from the test. As you mentioned, we shouldn't to verifying the internal fields
if len(b.serverIdentifierMap) != 1 { | ||
t.Fatalf("Builder.serverIdentifierMap has unexpected length %d, want 1", len(b.serverIdentifierMap)) | ||
} | ||
if b.serverIdentifierMap[serverID1] != tr1 { | ||
t.Fatalf("Builder.serverIdentifierMap[serverID1] = %v, want %v", b.serverIdentifierMap[serverID1], tr1) |
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 not a fan of tests that verify implementation details instead of verifying functionality. If we change the implementation for whatever reason without actually changing the provided functionality, this test would have to be rewritten. Instead I would recommend spawning real backends, and ensuring that new connections are created to them when a new transport is expected to be created (and no new connections are created when a transport is expected to be re-used). I would even recommend moving this test to grpctransport_test
package, thereby ensuring that you can never depend on the implementation.
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.
yeah i simplified this test to just create transports. The dedupe part is being tested in refcounted_transport_test.go which is using hooks similar internal clientImpl ref count tests.
for i, cfg := range a.xdsChannelConfigs { | ||
if isServerConfigEqual(sc, cfg.serverConfig) { | ||
return i | ||
} | ||
} |
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.
Within an authority, we should only be using a single server at a time. If it fails, we fall back to the next one
But when that happens, we still keep the higher priority server in the hopes that it will become ready. So, we a single authority could have more than one xDS channel. Only one can be active though.
xc, cleanup, err := a.getChannelForADS(sc, a) | ||
if err != nil { | ||
return nil, err | ||
} |
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.
was there any reason why we didn't do the client with failing transport?
Oversight I guess.
c879605
to
26f807f
Compare
yeah its not possible to have separate PRs with copy paste as they will fail the build. Basically if you see the commits, the one starting with copy are exact copy from internal xds client and then all the copy commits are followed by the modifications. So, you can review the modifications. For 3 files you mentioned this is the copy commit 94b4135 and the next commit 35cd65e is modifications. Ofcourse they were later trimmed a bit more based on doug's review but nothing major. Same pattern is followed for copied e2e 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: rename to examples_test.go
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
|
||
func ExampleServerIdentifierExtension() { | ||
si := clients.ServerIdentifier{ServerURI: "localhost:5678", Extensions: grpctransport.ServerIdentifierExtension{Credentials: "local"}} | ||
fmt.Printf("%v", si) |
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.
Or you can not print it.
Can you docstring examples to explain what they're attempting to show?
"google.golang.org/grpc/xds/internal/clients/grpctransport" | ||
) | ||
|
||
func ExampleServerIdentifierExtension() { |
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.
ExampleServerIdentifierExtension_HowToSetInServerIdentifier
or _Usage
or something?
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.
Looks like it requires to follow format "Example{struct name}" else it gives warning that the struct is not defined.
) | ||
|
||
func ExampleServerIdentifierExtension() { | ||
si := clients.ServerIdentifier{ServerURI: "localhost:5678", Extensions: grpctransport.ServerIdentifierExtension{Credentials: "local"}} |
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.
Add a comment: // Note the extensions field is set by value and not by pointer.
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
// It must be set by value (not pointer) in the | ||
// clients.ServerIdentifier.Extensions field (See Example). |
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.
same pointers with different values are equal
That's not possible. But (1) is the reason why using by value is preferred. Otherwise you have to reuse instances for equivalent values.
var creds credentials.Bundle | ||
if creds, ok = b.Credentials[sce.Credentials]; !ok { |
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: this would more commonly be written to use type inference:
creds, ok := b.Credentials[sce.Credentials]
if !ok {
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
} | ||
|
||
type grpcTransport struct { | ||
cc *grpc.ClientConn | ||
cc *grpc.ClientConn | ||
refCount int32 // accessed atomically |
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.
If atomic, then it should be atomic.Int32
. But you need a lock otherwise there will be races between transport re-use and dereferencing. In which case you don't need an atomic here.
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.
incrRef and decRef were doing atomic add and subtract so the var itself need not be atomic? But yeah you correctly pointed out the race condition of deleting the transport and at the someone else getting it. I have switched to lock so now both decrement and deletion are serialized.
if g.decrRef() != 0 { | ||
return nil | ||
} | ||
|
||
g.deleteFromServerIdentiferMap() |
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.
Yeah this is a race. If you dec the counter then take the lock, then re-use happens, then you're going to delete a used transport. Best to take the lock and remove the atomic and keep things simple. Performance does not matter on these code paths.
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.
yeah fixed using lock. Thanks for pointing it out.
tr.closed.Do(func() { | ||
err = tr.grpcTransport.Close() | ||
}) |
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 think we can guarantee this is only called once, so the Once shouldn't be needed. No?
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.
How will you guarantee? I think we can guarantee in our implementation may be but this transportRef is returned to the user of grpcTransport which can be used by anyone who is using our xdsclient so we should make sure its idempotent.
// Override the gRPC transport creation hook to get notified. | ||
origGRPCTransportCreateHook := grpcTransportCreateHook | ||
grpcTransportCreateCh := testutils.NewChannelWithSize(1) | ||
grpcTransportCreateHook = func() { | ||
grpcTransportCreateCh.Replace(struct{}{}) | ||
} | ||
defer func() { grpcTransportCreateHook = origGRPCTransportCreateHook }() |
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.
Maybe you don't need the hooks if you just check the pointer of the returned transport to confirm you are getting the same one when expected, or a new when when re-use is not expected?
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 am not sure if that's a better alternative. As @easwars pointed out, then we are being tied to implementation for assertion. So, in this case we will have a get method in transportReference which will return underlying transport but then to compare we will have to type cast etc. to compare. Also, for close we still need a hook? So, better to be consistent? Do you see any big limitation with hooks? Ofcourse a code change can disturb the order of them but that still has to go through a PR review.
1fd3ff5
to
eea4f8b
Compare
This is the change to make generic xDS client do resource watching from xDS management server. The xDS client uses the ADS (Aggregated Data Service) stream transport channel that was created in #8144. The e2e tests are written using the listener resource type from #8144 and grpctransport from #8066.
The PR copies the existing
clientimpl
,authority
,clientimpl_watcher
from internal xdsclient code along with the helpers that are part of gRPC and then modify them to use the generic client types and interfaces.The PR also adds
String()
andEqual()
methods forServerIdentifierExtension
and implements them forgrpctransport.ServerIdentifierExtension
. It also adds a custom map implementation similar toresolver.AddrMap
to re-use existing channels for similar server configurations.Recommend to review each commit of the PR separately in the order.
Commits with copy prefix are copy paste of internal xdsclient code and are followed by modifications. Review only the modification commit.
RELEASE NOTES: None