google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/tests/ads_stream_flow_control_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 "errors" 24 "slices" 25 "sort" 26 "testing" 27 "time" 28 29 "github.com/google/uuid" 30 "google.golang.org/grpc" 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 xdsclientinternal "google.golang.org/grpc/xds/internal/xdsclient/internal" 35 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 36 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 37 38 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 39 v3adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 40 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 41 ) 42 43 // blockingListenerWatcher implements xdsresource.ListenerWatcher. It writes to 44 // a channel when it receives a callback from the watch. It also makes the 45 // DoneNotifier passed to the callback available to the test, thereby enabling 46 // the test to block this watcher for as long as required. 47 type blockingListenerWatcher struct { 48 doneNotifierCh chan xdsresource.OnDoneFunc // DoneNotifier passed to the callback. 49 updateCh chan struct{} // Written to when an update is received. 50 errorCh chan struct{} // Written to when an error is received. 51 notFoundCh chan struct{} // Written to when the resource is not found. 52 } 53 54 func newBLockingListenerWatcher() *blockingListenerWatcher { 55 return &blockingListenerWatcher{ 56 doneNotifierCh: make(chan xdsresource.OnDoneFunc, 1), 57 updateCh: make(chan struct{}, 1), 58 errorCh: make(chan struct{}, 1), 59 notFoundCh: make(chan struct{}, 1), 60 } 61 } 62 63 func (lw *blockingListenerWatcher) OnUpdate(update *xdsresource.ListenerResourceData, done xdsresource.OnDoneFunc) { 64 // Notify receipt of the update. 65 select { 66 case lw.updateCh <- struct{}{}: 67 default: 68 } 69 70 select { 71 case lw.doneNotifierCh <- done: 72 default: 73 } 74 } 75 76 func (lw *blockingListenerWatcher) OnError(err error, done xdsresource.OnDoneFunc) { 77 // Notify receipt of an error. 78 select { 79 case lw.errorCh <- struct{}{}: 80 default: 81 } 82 83 select { 84 case lw.doneNotifierCh <- done: 85 default: 86 } 87 } 88 89 func (lw *blockingListenerWatcher) OnResourceDoesNotExist(done xdsresource.OnDoneFunc) { 90 // Notify receipt of resource not found. 91 select { 92 case lw.notFoundCh <- struct{}{}: 93 default: 94 } 95 96 select { 97 case lw.doneNotifierCh <- done: 98 default: 99 } 100 } 101 102 type wrappedADSStream struct { 103 v3adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient 104 recvCh chan struct{} 105 doneCh <-chan struct{} 106 } 107 108 func newWrappedADSStream(stream v3adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient, doneCh <-chan struct{}) *wrappedADSStream { 109 return &wrappedADSStream{ 110 AggregatedDiscoveryService_StreamAggregatedResourcesClient: stream, 111 recvCh: make(chan struct{}, 1), 112 doneCh: doneCh, 113 } 114 } 115 116 func (w *wrappedADSStream) Recv() (*v3discoverypb.DiscoveryResponse, error) { 117 select { 118 case w.recvCh <- struct{}{}: 119 case <-w.doneCh: 120 return nil, errors.New("Recv() called after the test has finished") 121 } 122 return w.AggregatedDiscoveryService_StreamAggregatedResourcesClient.Recv() 123 } 124 125 // Overrides the function to create a new ADS stream (used by the xdsclient 126 // transport), and returns a wrapped ADS stream, where the test can monitor 127 // Recv() calls. 128 func overrideADSStreamCreation(t *testing.T) chan *wrappedADSStream { 129 t.Helper() 130 131 adsStreamCh := make(chan *wrappedADSStream, 1) 132 origNewADSStream := xdsclientinternal.NewADSStream 133 xdsclientinternal.NewADSStream = func(ctx context.Context, cc *grpc.ClientConn) (v3adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient, error) { 134 s, err := v3adsgrpc.NewAggregatedDiscoveryServiceClient(cc).StreamAggregatedResources(ctx) 135 if err != nil { 136 return nil, err 137 } 138 ws := newWrappedADSStream(s, ctx.Done()) 139 select { 140 case adsStreamCh <- ws: 141 default: 142 } 143 return ws, nil 144 } 145 t.Cleanup(func() { xdsclientinternal.NewADSStream = origNewADSStream }) 146 return adsStreamCh 147 } 148 149 // Creates an xDS client with the given bootstrap contents. 150 func createXDSClient(t *testing.T, bootstrapContents []byte) xdsclient.XDSClient { 151 t.Helper() 152 153 config, err := bootstrap.NewConfigFromContents(bootstrapContents) 154 if err != nil { 155 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err) 156 } 157 pool := xdsclient.NewPool(config) 158 client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ 159 Name: t.Name(), 160 }) 161 if err != nil { 162 t.Fatalf("Failed to create xDS client: %v", err) 163 } 164 t.Cleanup(close) 165 return client 166 } 167 168 // Tests ADS stream level flow control with a single resource. The test does the 169 // following: 170 // - Starts a management server and configures a listener resource on it. 171 // - Creates an xDS client to the above management server, starts a couple of 172 // listener watchers for the above resource, and verifies that the update 173 // reaches these watchers. 174 // - These watchers don't invoke the onDone callback until explicitly 175 // triggered by the test. This allows the test to verify that the next 176 // Recv() call on the ADS stream does not happen until both watchers have 177 // completely processed the update, i.e invoke the onDone callback. 178 // - Resource is updated on the management server, and the test verifies that 179 // the update reaches the watchers. 180 func (s) TestADSFlowControl_ResourceUpdates_SingleResource(t *testing.T) { 181 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 182 defer cancel() 183 184 // Override the ADS stream creation. 185 adsStreamCh := overrideADSStreamCreation(t) 186 187 // Start an xDS management server. 188 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 189 190 // Create bootstrap configuration pointing to the above management server. 191 nodeID := uuid.New().String() 192 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 193 194 // Create an xDS client with the above bootstrap contents. 195 client := createXDSClient(t, bc) 196 197 // Configure two watchers for the same listener resource. 198 const listenerResourceName = "test-listener-resource" 199 const routeConfigurationName = "test-route-configuration-resource" 200 watcher1 := newBLockingListenerWatcher() 201 cancel1 := xdsresource.WatchListener(client, listenerResourceName, watcher1) 202 defer cancel1() 203 watcher2 := newBLockingListenerWatcher() 204 cancel2 := xdsresource.WatchListener(client, listenerResourceName, watcher2) 205 defer cancel2() 206 207 // Wait for the wrapped ADS stream to be created. 208 var adsStream *wrappedADSStream 209 select { 210 case adsStream = <-adsStreamCh: 211 case <-ctx.Done(): 212 t.Fatalf("Timed out waiting for ADS stream to be created") 213 } 214 215 // Configure the listener resource on the management server. 216 resources := e2e.UpdateOptions{ 217 NodeID: nodeID, 218 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)}, 219 SkipValidation: true, 220 } 221 if err := mgmtServer.Update(ctx, resources); err != nil { 222 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 223 } 224 225 // Ensure that there is a read on the stream. 226 select { 227 case <-adsStream.recvCh: 228 case <-ctx.Done(): 229 t.Fatalf("Timed out waiting for ADS stream to be read from") 230 } 231 232 // Wait for the update to reach the watchers. 233 select { 234 case <-watcher1.updateCh: 235 case <-ctx.Done(): 236 t.Fatalf("Timed out waiting for update to reach watcher 1") 237 } 238 select { 239 case <-watcher2.updateCh: 240 case <-ctx.Done(): 241 t.Fatalf("Timed out waiting for update to reach watcher 2") 242 } 243 244 // Update the listener resource on the management server to point to a new 245 // route configuration resource. 246 resources = e2e.UpdateOptions{ 247 NodeID: nodeID, 248 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, "new-route")}, 249 SkipValidation: true, 250 } 251 if err := mgmtServer.Update(ctx, resources); err != nil { 252 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 253 } 254 255 // Unblock one watcher. 256 onDone := <-watcher1.doneNotifierCh 257 onDone() 258 259 // Wait for a short duration and ensure that there is no read on the stream. 260 select { 261 case <-adsStream.recvCh: 262 t.Fatal("Recv() called on the ADS stream before all watchers have processed the previous update") 263 case <-time.After(defaultTestShortTimeout): 264 } 265 266 // Unblock the second watcher. 267 onDone = <-watcher2.doneNotifierCh 268 onDone() 269 270 // Ensure that there is a read on the stream, now that the previous update 271 // has been consumed by all watchers. 272 select { 273 case <-adsStream.recvCh: 274 case <-ctx.Done(): 275 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream after all watchers have processed the previous update") 276 } 277 278 // Wait for the new update to reach the watchers. 279 select { 280 case <-watcher1.updateCh: 281 case <-ctx.Done(): 282 t.Fatalf("Timed out waiting for update to reach watcher 1") 283 } 284 select { 285 case <-watcher2.updateCh: 286 case <-ctx.Done(): 287 t.Fatalf("Timed out waiting for update to reach watcher 2") 288 } 289 290 // At this point, the xDS client is shut down (and the associated transport 291 // is closed) without the watchers invoking their respective onDone 292 // callbacks. This verifies that the closing a transport that has pending 293 // watchers does not block. 294 } 295 296 // Tests ADS stream level flow control with a multiple resources. The test does 297 // the following: 298 // - Starts a management server and configures two listener resources on it. 299 // - Creates an xDS client to the above management server, starts a couple of 300 // listener watchers for the two resources, and verifies that the update 301 // reaches these watchers. 302 // - These watchers don't invoke the onDone callback until explicitly 303 // triggered by the test. This allows the test to verify that the next 304 // Recv() call on the ADS stream does not happen until both watchers have 305 // completely processed the update, i.e invoke the onDone callback. 306 func (s) TestADSFlowControl_ResourceUpdates_MultipleResources(t *testing.T) { 307 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 308 defer cancel() 309 310 // Override the ADS stream creation. 311 adsStreamCh := overrideADSStreamCreation(t) 312 313 // Start an xDS management server. 314 const listenerResourceName1 = "test-listener-resource-1" 315 const listenerResourceName2 = "test-listener-resource-2" 316 wantResourceNames := []string{listenerResourceName1, listenerResourceName2} 317 requestCh := make(chan struct{}, 1) 318 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 319 OnStreamRequest: func(id int64, req *v3discoverypb.DiscoveryRequest) error { 320 if req.GetTypeUrl() != version.V3ListenerURL { 321 return nil 322 } 323 gotResourceNames := req.GetResourceNames() 324 sort.Slice(gotResourceNames, func(i, j int) bool { return req.ResourceNames[i] < req.ResourceNames[j] }) 325 if slices.Equal(gotResourceNames, wantResourceNames) { 326 // The two resource names will be part of the initial request 327 // and also the ACK. Hence, we need to make this write 328 // non-blocking. 329 select { 330 case requestCh <- struct{}{}: 331 default: 332 } 333 } 334 return nil 335 }, 336 }) 337 338 // Create bootstrap configuration pointing to the above management server. 339 nodeID := uuid.New().String() 340 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 341 342 // Create an xDS client with the above bootstrap contents. 343 client := createXDSClient(t, bc) 344 345 // Configure two watchers for two different listener resources. 346 const routeConfigurationName1 = "test-route-configuration-resource-1" 347 watcher1 := newBLockingListenerWatcher() 348 cancel1 := xdsresource.WatchListener(client, listenerResourceName1, watcher1) 349 defer cancel1() 350 const routeConfigurationName2 = "test-route-configuration-resource-2" 351 watcher2 := newBLockingListenerWatcher() 352 cancel2 := xdsresource.WatchListener(client, listenerResourceName2, watcher2) 353 defer cancel2() 354 355 // Wait for the wrapped ADS stream to be created. 356 var adsStream *wrappedADSStream 357 select { 358 case adsStream = <-adsStreamCh: 359 case <-ctx.Done(): 360 t.Fatalf("Timed out waiting for ADS stream to be created") 361 } 362 363 // Ensure that there is a read on the stream. 364 select { 365 case <-adsStream.recvCh: 366 case <-ctx.Done(): 367 t.Fatalf("Timed out waiting for ADS stream to be read from") 368 } 369 370 // Wait for both resource names to be requested. 371 select { 372 case <-requestCh: 373 case <-ctx.Done(): 374 t.Fatal("Timed out waiting for both resource names to be requested") 375 } 376 377 // Configure the listener resources on the management server. 378 resources := e2e.UpdateOptions{ 379 NodeID: nodeID, 380 Listeners: []*v3listenerpb.Listener{ 381 e2e.DefaultClientListener(listenerResourceName1, routeConfigurationName1), 382 e2e.DefaultClientListener(listenerResourceName2, routeConfigurationName2), 383 }, 384 SkipValidation: true, 385 } 386 if err := mgmtServer.Update(ctx, resources); err != nil { 387 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 388 } 389 390 // At this point, we expect the management server to send both resources in 391 // the same response. So, both watchers would be notified at the same time, 392 // and no more Recv() calls should happen until both of them have invoked 393 // their respective onDone() callbacks. 394 395 // The order of callback invocations among the two watchers is not 396 // guaranteed. So, we select on both of them and unblock the first watcher 397 // whose callback is invoked. 398 var otherWatcherUpdateCh chan struct{} 399 var otherWatcherDoneCh chan xdsresource.OnDoneFunc 400 select { 401 case <-watcher1.updateCh: 402 onDone := <-watcher1.doneNotifierCh 403 onDone() 404 otherWatcherUpdateCh = watcher2.updateCh 405 otherWatcherDoneCh = watcher2.doneNotifierCh 406 case <-watcher2.updateCh: 407 onDone := <-watcher2.doneNotifierCh 408 onDone() 409 otherWatcherUpdateCh = watcher1.updateCh 410 otherWatcherDoneCh = watcher1.doneNotifierCh 411 case <-ctx.Done(): 412 t.Fatal("Timed out waiting for update to reach first watchers") 413 } 414 415 // Wait for a short duration and ensure that there is no read on the stream. 416 select { 417 case <-adsStream.recvCh: 418 t.Fatal("Recv() called on the ADS stream before all watchers have processed the previous update") 419 case <-time.After(defaultTestShortTimeout): 420 } 421 422 // Wait for the update on the second watcher and unblock it. 423 select { 424 case <-otherWatcherUpdateCh: 425 onDone := <-otherWatcherDoneCh 426 onDone() 427 case <-ctx.Done(): 428 t.Fatal("Timed out waiting for update to reach second watcher") 429 } 430 431 // Ensure that there is a read on the stream, now that the previous update 432 // has been consumed by all watchers. 433 select { 434 case <-adsStream.recvCh: 435 case <-ctx.Done(): 436 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream after all watchers have processed the previous update") 437 } 438 } 439 440 // Test ADS stream flow control with a single resource that is expected to be 441 // NACKed by the xDS client and the watcher's OnError() callback is expected to 442 // be invoked. Verifies that no further reads are attempted until the error is 443 // completely processed by the watcher. 444 func (s) TestADSFlowControl_ResourceErrors(t *testing.T) { 445 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 446 defer cancel() 447 448 // Override the ADS stream creation. 449 adsStreamCh := overrideADSStreamCreation(t) 450 451 // Start an xDS management server. 452 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 453 454 // Create bootstrap configuration pointing to the above management server. 455 nodeID := uuid.New().String() 456 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 457 458 // Create an xDS client with the above bootstrap contents. 459 client := createXDSClient(t, bc) 460 461 // Configure a watcher for a listener resource. 462 const listenerResourceName = "test-listener-resource" 463 watcher := newBLockingListenerWatcher() 464 cancel = xdsresource.WatchListener(client, listenerResourceName, watcher) 465 defer cancel() 466 467 // Wait for the wrapped ADS stream to be created. 468 var adsStream *wrappedADSStream 469 select { 470 case adsStream = <-adsStreamCh: 471 case <-ctx.Done(): 472 t.Fatalf("Timed out waiting for ADS stream to be created") 473 } 474 475 // Configure the management server to return a single listener resource 476 // which is expected to be NACKed by the client. 477 resources := e2e.UpdateOptions{ 478 NodeID: nodeID, 479 Listeners: []*v3listenerpb.Listener{badListenerResource(t, listenerResourceName)}, 480 SkipValidation: true, 481 } 482 if err := mgmtServer.Update(ctx, resources); err != nil { 483 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 484 } 485 486 // Ensure that there is a read on the stream. 487 select { 488 case <-adsStream.recvCh: 489 case <-ctx.Done(): 490 t.Fatalf("Timed out waiting for ADS stream to be read from") 491 } 492 493 // Wait for the error to reach the watcher. 494 select { 495 case <-watcher.errorCh: 496 case <-ctx.Done(): 497 t.Fatalf("Timed out waiting for error to reach watcher") 498 } 499 500 // Wait for a short duration and ensure that there is no read on the stream. 501 select { 502 case <-adsStream.recvCh: 503 t.Fatal("Recv() called on the ADS stream before all watchers have processed the previous update") 504 case <-time.After(defaultTestShortTimeout): 505 } 506 507 // Unblock one watcher. 508 onDone := <-watcher.doneNotifierCh 509 onDone() 510 511 // Ensure that there is a read on the stream, now that the previous error 512 // has been consumed by the watcher. 513 select { 514 case <-adsStream.recvCh: 515 case <-ctx.Done(): 516 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream after all watchers have processed the previous update") 517 } 518 } 519 520 // Test ADS stream flow control with a single resource that is deleted from the 521 // management server and therefore the watcher's OnResourceDoesNotExist() 522 // callback is expected to be invoked. Verifies that no further reads are 523 // attempted until the callback is completely handled by the watcher. 524 func (s) TestADSFlowControl_ResourceDoesNotExist(t *testing.T) { 525 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 526 defer cancel() 527 528 // Override the ADS stream creation. 529 adsStreamCh := overrideADSStreamCreation(t) 530 531 // Start an xDS management server. 532 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 533 534 // Create bootstrap configuration pointing to the above management server. 535 nodeID := uuid.New().String() 536 bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 537 538 // Create an xDS client with the above bootstrap contents. 539 client := createXDSClient(t, bc) 540 541 // Configure a watcher for a listener resource. 542 const listenerResourceName = "test-listener-resource" 543 const routeConfigurationName = "test-route-configuration-resource" 544 watcher := newBLockingListenerWatcher() 545 cancel = xdsresource.WatchListener(client, listenerResourceName, watcher) 546 defer cancel() 547 548 // Wait for the wrapped ADS stream to be created. 549 var adsStream *wrappedADSStream 550 select { 551 case adsStream = <-adsStreamCh: 552 case <-ctx.Done(): 553 t.Fatalf("Timed out waiting for ADS stream to be created") 554 } 555 556 // Configure the listener resource on the management server. 557 resources := e2e.UpdateOptions{ 558 NodeID: nodeID, 559 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)}, 560 SkipValidation: true, 561 } 562 if err := mgmtServer.Update(ctx, resources); err != nil { 563 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 564 } 565 566 // Ensure that there is a read on the stream. 567 select { 568 case <-adsStream.recvCh: 569 case <-ctx.Done(): 570 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream") 571 } 572 573 // Wait for the update to reach the watcher and unblock it. 574 select { 575 case <-watcher.updateCh: 576 onDone := <-watcher.doneNotifierCh 577 onDone() 578 case <-ctx.Done(): 579 t.Fatalf("Timed out waiting for update to reach watcher 1") 580 } 581 582 // Ensure that there is a read on the stream. 583 select { 584 case <-adsStream.recvCh: 585 case <-ctx.Done(): 586 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream") 587 } 588 589 // Remove the listener resource on the management server. 590 resources = e2e.UpdateOptions{ 591 NodeID: nodeID, 592 Listeners: []*v3listenerpb.Listener{}, 593 SkipValidation: true, 594 } 595 if err := mgmtServer.Update(ctx, resources); err != nil { 596 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 597 } 598 599 // Wait for the resource not found callback to be invoked. 600 select { 601 case <-watcher.notFoundCh: 602 case <-ctx.Done(): 603 t.Fatalf("Timed out waiting for resource not found callback to be invoked on the watcher") 604 } 605 606 // Wait for a short duration and ensure that there is no read on the stream. 607 select { 608 case <-adsStream.recvCh: 609 t.Fatal("Recv() called on the ADS stream before all watchers have processed the previous update") 610 case <-time.After(defaultTestShortTimeout): 611 } 612 613 // Unblock the watcher. 614 onDone := <-watcher.doneNotifierCh 615 onDone() 616 617 // Ensure that there is a read on the stream. 618 select { 619 case <-adsStream.recvCh: 620 case <-ctx.Done(): 621 t.Fatalf("Timed out waiting for Recv() to be called on the ADS stream") 622 } 623 }