google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/ads_stream_watch_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 "net" 25 "testing" 26 "time" 27 28 "github.com/google/uuid" 29 "google.golang.org/grpc/credentials/insecure" 30 "google.golang.org/grpc/internal/testutils/xds/e2e" 31 "google.golang.org/grpc/xds/internal/clients/grpctransport" 32 "google.golang.org/grpc/xds/internal/clients/internal/testutils" 33 "google.golang.org/grpc/xds/internal/clients/xdsclient" 34 xdsclientinternal "google.golang.org/grpc/xds/internal/clients/xdsclient/internal" 35 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 36 37 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 38 ) 39 40 func waitForResourceWatchState(ctx context.Context, client *xdsclient.XDSClient, resourceName string, wantState xdsresource.WatchState, wantTimer bool) error { 41 var lastErr error 42 for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { 43 err := verifyResourceWatchState(client, resourceName, wantState, wantTimer) 44 if err == nil { 45 break 46 } 47 lastErr = err 48 } 49 if ctx.Err() != nil { 50 return fmt.Errorf("timeout when waiting for expected watch state for resource %q: %v", resourceName, lastErr) 51 } 52 return nil 53 } 54 55 func verifyResourceWatchState(client *xdsclient.XDSClient, resourceName string, wantState xdsresource.WatchState, wantTimer bool) error { 56 resourceWatchStateForTesting := xdsclientinternal.ResourceWatchStateForTesting.(func(*xdsclient.XDSClient, xdsclient.ResourceType, string) (xdsresource.ResourceWatchState, error)) 57 gotState, err := resourceWatchStateForTesting(client, listenerType, resourceName) 58 if err != nil { 59 return fmt.Errorf("failed to get watch state for resource %q: %v", resourceName, err) 60 } 61 if gotState.State != wantState { 62 return fmt.Errorf("watch state for resource %q is %v, want %v", resourceName, gotState.State, wantState) 63 } 64 if (gotState.ExpiryTimer != nil) != wantTimer { 65 return fmt.Errorf("expiry timer for resource %q is %t, want %t", resourceName, gotState.ExpiryTimer != nil, wantTimer) 66 } 67 return nil 68 } 69 70 // Tests the state transitions of the resource specific watch state within the 71 // ADS stream, specifically when the stream breaks (for both resources that have 72 // been previously received and for resources that are yet to be received). 73 func (s) TestADS_WatchState_StreamBreaks(t *testing.T) { 74 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 75 defer cancel() 76 77 // Create an xDS management server with a restartable listener. 78 l, err := net.Listen("tcp", "localhost:0") 79 if err != nil { 80 t.Fatalf("net.Listen() failed: %v", err) 81 } 82 lis := testutils.NewRestartableListener(l) 83 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis}) 84 85 // Create an xDS client pointing to the above server. 86 nodeID := uuid.New().String() 87 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 88 client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs)) 89 90 // Create a watch for the first listener resource and verify that the timer 91 // is running and the watch state is `requested`. 92 const listenerName1 = "listener1" 93 ldsCancel1 := client.WatchResource(xdsresource.V3ListenerURL, listenerName1, noopListenerWatcher{}) 94 defer ldsCancel1() 95 if err := waitForResourceWatchState(ctx, client, listenerName1, xdsresource.ResourceWatchStateRequested, true); err != nil { 96 t.Fatal(err) 97 } 98 99 // Configure the first resource on the management server. This should result 100 // in the resource being pushed to the xDS client and should result in the 101 // timer getting stopped and the watch state moving to `received`. 102 const routeConfigName = "route-config" 103 listenerResource1 := e2e.DefaultClientListener(listenerName1, routeConfigName) 104 resources := e2e.UpdateOptions{ 105 NodeID: nodeID, 106 Listeners: []*v3listenerpb.Listener{listenerResource1}, 107 SkipValidation: true, 108 } 109 if err := mgmtServer.Update(ctx, resources); err != nil { 110 t.Fatal(err) 111 } 112 if err := waitForResourceWatchState(ctx, client, listenerName1, xdsresource.ResourceWatchStateReceived, false); err != nil { 113 t.Fatal(err) 114 } 115 116 // Create a watch for the second listener resource and verify that the timer 117 // is running and the watch state is `requested`. 118 const listenerName2 = "listener2" 119 ldsCancel2 := client.WatchResource(xdsresource.V3ListenerURL, listenerName2, noopListenerWatcher{}) 120 defer ldsCancel2() 121 if err := waitForResourceWatchState(ctx, client, listenerName2, xdsresource.ResourceWatchStateRequested, true); err != nil { 122 t.Fatal(err) 123 } 124 125 // Stop the server to break the ADS stream. Since the first resource was 126 // already received, this should not change anything for it. But for the 127 // second resource, it should result in the timer getting stopped and the 128 // watch state moving to `started`. 129 lis.Stop() 130 if err := waitForResourceWatchState(ctx, client, listenerName2, xdsresource.ResourceWatchStateStarted, false); err != nil { 131 t.Fatal(err) 132 } 133 if err := verifyResourceWatchState(client, listenerName1, xdsresource.ResourceWatchStateReceived, false); err != nil { 134 t.Fatal(err) 135 } 136 137 // Restart the server and verify that the timer is running and the watch 138 // state is `requested`, for the second resource. For the first resource, 139 // nothing should change. 140 lis.Restart() 141 if err := waitForResourceWatchState(ctx, client, listenerName2, xdsresource.ResourceWatchStateRequested, true); err != nil { 142 t.Fatal(err) 143 } 144 if err := verifyResourceWatchState(client, listenerName1, xdsresource.ResourceWatchStateReceived, false); err != nil { 145 t.Fatal(err) 146 } 147 148 // Configure the second resource on the management server. This should result 149 // in the resource being pushed to the xDS client and should result in the 150 // timer getting stopped and the watch state moving to `received`. 151 listenerResource2 := e2e.DefaultClientListener(listenerName2, routeConfigName) 152 resources = e2e.UpdateOptions{ 153 NodeID: nodeID, 154 Listeners: []*v3listenerpb.Listener{listenerResource1, listenerResource2}, 155 SkipValidation: true, 156 } 157 if err := mgmtServer.Update(ctx, resources); err != nil { 158 t.Fatal(err) 159 } 160 if err := waitForResourceWatchState(ctx, client, listenerName2, xdsresource.ResourceWatchStateReceived, false); err != nil { 161 t.Fatal(err) 162 } 163 } 164 165 // Tests the behavior of the xDS client when a resource watch timer expires and 166 // verifies the resource watch state transitions as expected. 167 func (s) TestADS_WatchState_TimerFires(t *testing.T) { 168 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 169 defer cancel() 170 171 // Start an xDS management server. 172 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 173 174 // Create an xDS client with bootstrap pointing to the above server, and a 175 // short resource expiry timeout. 176 nodeID := uuid.New().String() 177 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 178 overrideWatchExpiryTimeout(t, defaultTestWatchExpiryTimeout) 179 client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs)) 180 181 // Create a watch for the first listener resource and verify that the timer 182 // is running and the watch state is `requested`. 183 const listenerName = "listener" 184 ldsCancel1 := client.WatchResource(xdsresource.V3ListenerURL, listenerName, noopListenerWatcher{}) 185 defer ldsCancel1() 186 if err := waitForResourceWatchState(ctx, client, listenerName, xdsresource.ResourceWatchStateRequested, true); err != nil { 187 t.Fatal(err) 188 } 189 190 // Since the resource is not configured on the management server, the watch 191 // expiry timer is expected to fire, and the watch state should move to 192 // `timeout`. 193 if err := waitForResourceWatchState(ctx, client, listenerName, xdsresource.ResourceWatchStateTimeout, false); err != nil { 194 t.Fatal(err) 195 } 196 }