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