google.golang.org/grpc@v1.72.2/test/xds/xds_client_ack_nack_test.go (about) 1 /* 2 * 3 * Copyright 2022 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package xds_test 20 21 import ( 22 "context" 23 "fmt" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "github.com/google/uuid" 28 "google.golang.org/grpc" 29 "google.golang.org/grpc/credentials/insecure" 30 "google.golang.org/grpc/internal" 31 "google.golang.org/grpc/internal/grpcsync" 32 "google.golang.org/grpc/internal/stubserver" 33 "google.golang.org/grpc/internal/testutils" 34 "google.golang.org/grpc/internal/testutils/xds/e2e" 35 "google.golang.org/grpc/resolver" 36 37 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 38 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 39 testgrpc "google.golang.org/grpc/interop/grpc_testing" 40 testpb "google.golang.org/grpc/interop/grpc_testing" 41 ) 42 43 // We are interested in LDS, RDS, CDS and EDS resources as part of the regular 44 // xDS flow on the client. 45 const wantResources = 4 46 47 // seenAllACKs returns true if the provided ackVersions map contains valid acks 48 // for all the resources that we are interested in. If `wantNonEmpty` is true, 49 // only non-empty ack versions are considered valid. 50 func seenAllACKs(acksVersions map[string]string, wantNonEmpty bool) bool { 51 if len(acksVersions) != wantResources { 52 return false 53 } 54 for _, ack := range acksVersions { 55 if wantNonEmpty && ack == "" { 56 return false 57 } 58 } 59 return true 60 } 61 62 // TestClientResourceVersionAfterStreamRestart tests the scenario where the 63 // xdsClient's ADS stream to the management server gets broken. This test 64 // verifies that the version number on the initial request on the new stream 65 // indicates the most recent version seen by the client on the previous stream. 66 func (s) TestClientResourceVersionAfterStreamRestart(t *testing.T) { 67 // Create a restartable listener which can close existing connections. 68 l, err := testutils.LocalTCPListener() 69 if err != nil { 70 t.Fatalf("testutils.LocalTCPListener() failed: %v", err) 71 } 72 lis := testutils.NewRestartableListener(l) 73 74 // We depend on the fact that the management server assigns monotonically 75 // increasing stream IDs starting at 1. 76 const ( 77 idBeforeRestart = 1 78 idAfterRestart = 2 79 ) 80 81 // Events of importance in the test, in the order in which they are expected 82 // to happen. 83 acksReceivedBeforeRestart := grpcsync.NewEvent() 84 streamRestarted := grpcsync.NewEvent() 85 acksReceivedAfterRestart := grpcsync.NewEvent() 86 87 // Map from stream id to a map of resource type to resource version. 88 ackVersionsMap := make(map[int64]map[string]string) 89 managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 90 Listener: lis, 91 OnStreamRequest: func(id int64, req *v3discoverypb.DiscoveryRequest) error { 92 // Return early under the following circumstances: 93 // - Received all the requests we wanted to see. This is to avoid 94 // any stray requests leading to test flakes. 95 // - Request contains no resource names. Such requests are usually 96 // seen when the xdsclient is shutting down and is no longer 97 // interested in the resources that it had subscribed to earlier. 98 if acksReceivedAfterRestart.HasFired() || len(req.GetResourceNames()) == 0 { 99 return nil 100 } 101 // Create a stream specific map to store ack versions if this is the 102 // first time we are seeing this stream id. 103 if ackVersionsMap[id] == nil { 104 ackVersionsMap[id] = make(map[string]string) 105 } 106 ackVersionsMap[id][req.GetTypeUrl()] = req.GetVersionInfo() 107 // Prior to stream restart, we are interested only in non-empty 108 // resource versions. The xdsclient first sends out requests with an 109 // empty version string. After receipt of requested resource, it 110 // sends out another request for the same resource, but this time 111 // with a non-empty version string, to serve as an ACK. 112 if seenAllACKs(ackVersionsMap[idBeforeRestart], true) { 113 acksReceivedBeforeRestart.Fire() 114 } 115 // After stream restart, we expect the xdsclient to send out 116 // requests with version string set to the previously ACKed 117 // versions. If it sends out requests with empty version string, it 118 // is a bug and we want this test to catch it. 119 if seenAllACKs(ackVersionsMap[idAfterRestart], false) { 120 acksReceivedAfterRestart.Fire() 121 } 122 return nil 123 }, 124 OnStreamClosed: func(int64, *v3corepb.Node) { 125 streamRestarted.Fire() 126 }, 127 }) 128 129 // Create bootstrap configuration pointing to the above management server. 130 nodeID := uuid.New().String() 131 bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) 132 133 // Create an xDS resolver with the above bootstrap configuration. 134 if internal.NewXDSResolverWithConfigForTesting == nil { 135 t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil") 136 } 137 xdsResolver, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents) 138 if err != nil { 139 t.Fatalf("Failed to create xDS resolver for testing: %v", err) 140 } 141 142 server := stubserver.StartTestService(t, nil) 143 defer server.Stop() 144 145 const serviceName = "my-service-client-side-xds" 146 resources := e2e.DefaultClientResources(e2e.ResourceParams{ 147 DialTarget: serviceName, 148 NodeID: nodeID, 149 Host: "localhost", 150 Port: testutils.ParsePort(t, server.Address), 151 SecLevel: e2e.SecurityLevelNone, 152 }) 153 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 154 defer cancel() 155 if err := managementServer.Update(ctx, resources); err != nil { 156 t.Fatal(err) 157 } 158 159 // Create a ClientConn and make a successful RPC. 160 cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver)) 161 if err != nil { 162 t.Fatalf("failed to dial local test server: %v", err) 163 } 164 defer cc.Close() 165 166 client := testgrpc.NewTestServiceClient(cc) 167 if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil { 168 t.Fatalf("rpc EmptyCall() failed: %v", err) 169 } 170 171 // A successful RPC means that the xdsclient received all requested 172 // resources. The ACKs from the xdsclient may get a little delayed. So, we 173 // need to wait for all ACKs to be received on the management server before 174 // restarting the stream. 175 select { 176 case <-ctx.Done(): 177 t.Fatal("Timeout when waiting for all resources to be ACKed prior to stream restart") 178 case <-acksReceivedBeforeRestart.Done(): 179 } 180 181 // Stop the listener on the management server. This will cause the client to 182 // backoff and recreate the stream. 183 lis.Stop() 184 185 // Wait for the stream to be closed on the server. 186 <-streamRestarted.Done() 187 188 // Restart the listener on the management server to be able to accept 189 // reconnect attempts from the client. 190 lis.Restart() 191 192 // Wait for all the previously sent resources to be re-requested. 193 select { 194 case <-ctx.Done(): 195 t.Fatal("Timeout when waiting for all resources to be ACKed post stream restart") 196 case <-acksReceivedAfterRestart.Done(): 197 } 198 199 if diff := cmp.Diff(ackVersionsMap[idBeforeRestart], ackVersionsMap[idAfterRestart]); diff != "" { 200 t.Fatalf("unexpected diff in ack versions before and after stream restart (-want, +got):\n%s", diff) 201 } 202 if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil { 203 t.Fatalf("rpc EmptyCall() failed: %v", err) 204 } 205 }