google.golang.org/grpc@v1.74.2/xds/internal/xdsclient/tests/ads_stream_restart_test.go (about) 1 /* 2 * 3 * Copyright 2024 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 xdsclient_test 20 21 import ( 22 "context" 23 "fmt" 24 "testing" 25 "time" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/google/go-cmp/cmp/cmpopts" 29 "github.com/google/uuid" 30 "google.golang.org/grpc/internal/testutils" 31 "google.golang.org/grpc/internal/testutils/xds/e2e" 32 "google.golang.org/grpc/internal/xds/bootstrap" 33 "google.golang.org/grpc/xds/internal/xdsclient" 34 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 35 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 36 37 v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 38 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 39 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 40 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 41 ) 42 43 // waitForResourceNames waits for the wantNames to be received on namesCh. 44 // Returns a non-nil error if the context expires before that. 45 func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) error { 46 t.Helper() 47 48 var lastRequestedNames []string 49 for ; ; <-time.After(defaultTestShortTimeout) { 50 select { 51 case <-ctx.Done(): 52 return fmt.Errorf("timeout waiting for resources %v to be requested from the management server. Last requested resources: %v", wantNames, lastRequestedNames) 53 case gotNames := <-namesCh: 54 if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) { 55 return nil 56 } 57 lastRequestedNames = gotNames 58 } 59 } 60 } 61 62 // Tests that an ADS stream is restarted after a connection failure. Also 63 // verifies that if there were any watches registered before the connection 64 // failed, those resources are re-requested after the stream is restarted. 65 func (s) TestADS_ResourcesAreRequestedAfterStreamRestart(t *testing.T) { 66 // Create a restartable listener that can simulate a broken ADS stream. 67 l, err := testutils.LocalTCPListener() 68 if err != nil { 69 t.Fatalf("net.Listen() failed: %v", err) 70 } 71 lis := testutils.NewRestartableListener(l) 72 73 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 74 defer cancel() 75 76 // Start an xDS management server that uses a couple of channels to inform 77 // the test about the specific LDS and CDS resource names being requested. 78 ldsResourcesCh := make(chan []string, 1) 79 cdsResourcesCh := make(chan []string, 1) 80 streamOpened := make(chan struct{}, 1) 81 streamClosed := make(chan struct{}, 1) 82 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 83 Listener: lis, 84 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 85 t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl()) 86 87 // Drain the resource name channels before writing to them to ensure 88 // that the most recently requested names are made available to the 89 // test. 90 switch req.GetTypeUrl() { 91 case version.V3ClusterURL: 92 select { 93 case <-cdsResourcesCh: 94 default: 95 } 96 cdsResourcesCh <- req.GetResourceNames() 97 case version.V3ListenerURL: 98 select { 99 case <-ldsResourcesCh: 100 default: 101 } 102 ldsResourcesCh <- req.GetResourceNames() 103 } 104 return nil 105 }, 106 OnStreamClosed: func(int64, *v3corepb.Node) { 107 select { 108 case streamClosed <- struct{}{}: 109 default: 110 } 111 112 }, 113 OnStreamOpen: func(context.Context, int64, string) error { 114 select { 115 case streamOpened <- struct{}{}: 116 default: 117 } 118 return nil 119 }, 120 }) 121 122 // Create a listener resource on the management server. 123 const listenerName = "listener" 124 const routeConfigName = "route-config" 125 nodeID := uuid.New().String() 126 resources := e2e.UpdateOptions{ 127 NodeID: nodeID, 128 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerName, routeConfigName)}, 129 SkipValidation: true, 130 } 131 if err := mgmtServer.Update(ctx, resources); err != nil { 132 t.Fatal(err) 133 } 134 135 // Create bootstrap configuration pointing to the above management server. 136 bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 137 138 // Create an xDS client with the above bootstrap configuration. 139 config, err := bootstrap.NewConfigFromContents(bootstrapContents) 140 if err != nil { 141 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err) 142 } 143 pool := xdsclient.NewPool(config) 144 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 145 Name: t.Name(), 146 }) 147 if err != nil { 148 t.Fatalf("Failed to create xDS client: %v", err) 149 } 150 defer close() 151 152 // Register a watch for a listener resource. 153 lw := newListenerWatcher() 154 ldsCancel := xdsresource.WatchListener(client, listenerName, lw) 155 defer ldsCancel() 156 157 // Verify that an ADS stream is opened and an LDS request with the above 158 // resource name is sent. 159 select { 160 case <-streamOpened: 161 case <-ctx.Done(): 162 t.Fatal("Timeout when waiting for ADS stream to open") 163 } 164 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName}); err != nil { 165 t.Fatal(err) 166 } 167 168 // Verify the update received by the watcher. 169 wantListenerUpdate := listenerUpdateErrTuple{ 170 update: xdsresource.ListenerUpdate{ 171 RouteConfigName: routeConfigName, 172 HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, 173 }, 174 } 175 if err := verifyListenerUpdate(ctx, lw.updateCh, wantListenerUpdate); err != nil { 176 t.Fatal(err) 177 } 178 179 // Create a cluster resource on the management server, in addition to the 180 // existing listener resource. 181 const clusterName = "cluster" 182 resources = e2e.UpdateOptions{ 183 NodeID: nodeID, 184 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerName, routeConfigName)}, 185 Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, clusterName, e2e.SecurityLevelNone)}, 186 SkipValidation: true, 187 } 188 if err := mgmtServer.Update(ctx, resources); err != nil { 189 t.Fatal(err) 190 } 191 192 // Register a watch for a cluster resource, and verify that a CDS request 193 // with the above resource name is sent. 194 cw := newClusterWatcher() 195 cdsCancel := xdsresource.WatchCluster(client, clusterName, cw) 196 if err := waitForResourceNames(ctx, t, cdsResourcesCh, []string{clusterName}); err != nil { 197 t.Fatal(err) 198 } 199 200 // Verify the update received by the watcher. 201 wantClusterUpdate := clusterUpdateErrTuple{ 202 update: xdsresource.ClusterUpdate{ 203 ClusterName: clusterName, 204 EDSServiceName: clusterName, 205 }, 206 } 207 if err := verifyClusterUpdate(ctx, cw.updateCh, wantClusterUpdate); err != nil { 208 t.Fatal(err) 209 } 210 211 // Cancel the watch for the above cluster resource, and verify that a CDS 212 // request with no resource names is sent. 213 cdsCancel() 214 if err := waitForResourceNames(ctx, t, cdsResourcesCh, []string{}); err != nil { 215 t.Fatal(err) 216 } 217 218 // Stop the restartable listener and wait for the stream to close. 219 lis.Stop() 220 select { 221 case <-streamClosed: 222 case <-ctx.Done(): 223 t.Fatal("Timeout when waiting for ADS stream to close") 224 } 225 226 // Restart the restartable listener and wait for the stream to open. 227 lis.Restart() 228 select { 229 case <-streamOpened: 230 case <-ctx.Done(): 231 t.Fatal("Timeout when waiting for ADS stream to open") 232 } 233 234 // Verify that the listener resource is requested again. 235 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName}); err != nil { 236 t.Fatal(err) 237 } 238 239 // Wait for a short duration and verify that no CDS request is sent, since 240 // there are no resources being watched. 241 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 242 defer sCancel() 243 select { 244 case <-sCtx.Done(): 245 case names := <-cdsResourcesCh: 246 t.Fatalf("CDS request sent for resource names %v, when expecting no request", names) 247 } 248 }