google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/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 "net" 24 "testing" 25 26 "github.com/google/uuid" 27 "google.golang.org/grpc/credentials/insecure" 28 "google.golang.org/grpc/internal/testutils/xds/e2e" 29 "google.golang.org/grpc/xds/internal/clients/grpctransport" 30 "google.golang.org/grpc/xds/internal/clients/internal/testutils" 31 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 32 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 33 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 := net.Listen("tcp", "localhost:0") 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, 2) 56 streamOpened := make(chan struct{}, 1) 57 streamClosed := make(chan struct{}, 1) 58 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 59 Listener: lis, 60 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 61 t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl()) 62 63 // Drain the resource name channels before writing to them to ensure 64 // that the most recently requested names are made available to the 65 // test. 66 if req.GetTypeUrl() == version.V3ListenerURL { 67 select { 68 case <-ldsResourcesCh: 69 default: 70 } 71 ldsResourcesCh <- req.GetResourceNames() 72 } 73 return nil 74 }, 75 OnStreamClosed: func(int64, *v3corepb.Node) { 76 select { 77 case streamClosed <- struct{}{}: 78 default: 79 } 80 81 }, 82 OnStreamOpen: func(context.Context, int64, string) error { 83 select { 84 case streamOpened <- struct{}{}: 85 default: 86 } 87 return nil 88 }, 89 }) 90 91 // Create a listener resource on the management server. 92 const listenerName = "listener" 93 const routeConfigName = "route-config" 94 nodeID := uuid.New().String() 95 resources := e2e.UpdateOptions{ 96 NodeID: nodeID, 97 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerName, routeConfigName)}, 98 SkipValidation: true, 99 } 100 if err := mgmtServer.Update(ctx, resources); err != nil { 101 t.Fatal(err) 102 } 103 104 // Create an xDS client pointing to the above server. 105 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 106 client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs)) 107 108 // Register a watch for a listener resource. 109 lw := newListenerWatcher() 110 ldsCancel := client.WatchResource(xdsresource.V3ListenerURL, listenerName, lw) 111 defer ldsCancel() 112 113 // Verify that an ADS stream is opened and an LDS request with the above 114 // resource name is sent. 115 select { 116 case <-streamOpened: 117 case <-ctx.Done(): 118 t.Fatal("Timeout when waiting for ADS stream to open") 119 } 120 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName}); err != nil { 121 t.Fatal(err) 122 } 123 124 // Verify the update received by the watcher. 125 wantListenerUpdate := listenerUpdateErrTuple{ 126 update: listenerUpdate{ 127 RouteConfigName: routeConfigName, 128 }, 129 } 130 if err := verifyListenerUpdate(ctx, lw.updateCh, wantListenerUpdate); err != nil { 131 t.Fatal(err) 132 } 133 134 // Create another listener resource on the management server, in addition 135 // to the existing listener resource. 136 const listenerName2 = "listener2" 137 const routeConfigName2 = "route-config2" 138 resources = e2e.UpdateOptions{ 139 NodeID: nodeID, 140 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerName, routeConfigName), e2e.DefaultClientListener(listenerName2, routeConfigName2)}, 141 SkipValidation: true, 142 } 143 if err := mgmtServer.Update(ctx, resources); err != nil { 144 t.Fatal(err) 145 } 146 147 // Register a watch for another listener resource, and verify that a LDS request 148 // with the both listener resource names are sent. 149 lw2 := newListenerWatcher() 150 ldsCancel2 := client.WatchResource(xdsresource.V3ListenerURL, listenerName2, lw2) 151 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName, listenerName2}); err != nil { 152 t.Fatal(err) 153 } 154 155 // Verify the update received by the watcher. 156 wantListenerUpdate = listenerUpdateErrTuple{ 157 update: listenerUpdate{ 158 RouteConfigName: routeConfigName2, 159 }, 160 } 161 if err := verifyListenerUpdate(ctx, lw2.updateCh, wantListenerUpdate); err != nil { 162 t.Fatal(err) 163 } 164 165 // Cancel the watch for the second listener resource, and verify that an LDS 166 // request with only first listener resource names is sent. 167 ldsCancel2() 168 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName}); err != nil { 169 t.Fatal(err) 170 } 171 172 // Stop the restartable listener and wait for the stream to close. 173 lis.Stop() 174 select { 175 case <-streamClosed: 176 case <-ctx.Done(): 177 t.Fatal("Timeout when waiting for ADS stream to close") 178 } 179 180 // Restart the restartable listener and wait for the stream to open. 181 lis.Restart() 182 select { 183 case <-streamOpened: 184 case <-ctx.Done(): 185 t.Fatal("Timeout when waiting for ADS stream to open") 186 } 187 188 // Verify that the first listener resource is requested again. 189 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerName}); err != nil { 190 t.Fatal(err) 191 } 192 193 // Wait for a short duration and verify that no LDS request is sent, since 194 // there are no resources being watched. 195 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 196 defer sCancel() 197 select { 198 case <-sCtx.Done(): 199 case names := <-ldsResourcesCh: 200 t.Fatalf("LDS request sent for resource names %v, when expecting no request", names) 201 } 202 }