google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/ads_stream_ack_nack_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  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/uuid"
    30  	"google.golang.org/grpc/credentials/insecure"
    31  	"google.golang.org/grpc/xds/internal/clients"
    32  	"google.golang.org/grpc/xds/internal/clients/grpctransport"
    33  	"google.golang.org/grpc/xds/internal/clients/internal/testutils"
    34  	"google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e"
    35  	"google.golang.org/grpc/xds/internal/clients/xdsclient"
    36  	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
    37  	"google.golang.org/protobuf/proto"
    38  	"google.golang.org/protobuf/testing/protocmp"
    39  
    40  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    41  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    42  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    43  )
    44  
    45  // Creates an xDS client with the given management server address, node ID
    46  // and transport builder.
    47  func createXDSClient(t *testing.T, mgmtServerAddress string, nodeID string, transportBuilder clients.TransportBuilder) *xdsclient.XDSClient {
    48  	t.Helper()
    49  
    50  	resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType}
    51  	si := clients.ServerIdentifier{
    52  		ServerURI:  mgmtServerAddress,
    53  		Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"},
    54  	}
    55  
    56  	xdsClientConfig := xdsclient.Config{
    57  		Servers:          []xdsclient.ServerConfig{{ServerIdentifier: si}},
    58  		Node:             clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"},
    59  		TransportBuilder: transportBuilder,
    60  		ResourceTypes:    resourceTypes,
    61  		// Xdstp resource names used in this test do not specify an
    62  		// authority. These will end up looking up an entry with the
    63  		// empty key in the authorities map. Having an entry with an
    64  		// empty key and empty configuration, results in these
    65  		// resources also using the top-level configuration.
    66  		Authorities: map[string]xdsclient.Authority{
    67  			"": {XDSServers: []xdsclient.ServerConfig{}},
    68  		},
    69  	}
    70  
    71  	// Create an xDS client with the above config.
    72  	client, err := xdsclient.New(xdsClientConfig)
    73  	if err != nil {
    74  		t.Fatalf("Failed to create xDS client: %v", err)
    75  	}
    76  	t.Cleanup(func() { client.Close() })
    77  	return client
    78  }
    79  
    80  // Tests simple ACK and NACK scenarios on the ADS stream:
    81  //  1. When a good response is received, i.e. once that is expected to be ACKed,
    82  //     the test verifies that an ACK is sent matching the version and nonce from
    83  //     the response.
    84  //  2. When a subsequent bad response is received, i.e. once is expected to be
    85  //     NACKed, the test verifies that a NACK is sent matching the previously
    86  //     ACKed version and current nonce from the response.
    87  //  3. When a subsequent good response is received, the test verifies that an
    88  //     ACK is sent matching the version and nonce from the current response.
    89  func (s) TestADS_ACK_NACK_Simple(t *testing.T) {
    90  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    91  	defer cancel()
    92  
    93  	// Create an xDS management server listening on a local port. Configure the
    94  	// request and response handlers to push on channels that are inspected by
    95  	// the test goroutine to verify ACK version and nonce.
    96  	streamRequestCh := testutils.NewChannelWithSize(1)
    97  	streamResponseCh := testutils.NewChannelWithSize(1)
    98  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
    99  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   100  			streamRequestCh.SendContext(ctx, req)
   101  			return nil
   102  		},
   103  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   104  			streamResponseCh.SendContext(ctx, resp)
   105  		},
   106  	})
   107  
   108  	// Create a listener resource on the management server.
   109  	const listenerName = "listener"
   110  	const routeConfigName = "route-config"
   111  	nodeID := uuid.New().String()
   112  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
   113  	resources := e2e.UpdateOptions{
   114  		NodeID:         nodeID,
   115  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   116  		SkipValidation: true,
   117  	}
   118  	if err := mgmtServer.Update(ctx, resources); err != nil {
   119  		t.Fatal(err)
   120  	}
   121  
   122  	// Create an xDS client pointing to the above server.
   123  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   124  	client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs))
   125  
   126  	// Register a watch for a listener resource.
   127  	lw := newListenerWatcher()
   128  	ldsCancel := client.WatchResource(xdsresource.V3ListenerURL, listenerName, lw)
   129  	defer ldsCancel()
   130  
   131  	// Verify that the initial discovery request matches expectation.
   132  	r, err := streamRequestCh.Receive(ctx)
   133  	if err != nil {
   134  		t.Fatal("Timeout when waiting for the initial discovery request")
   135  	}
   136  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   137  	wantReq := &v3discoverypb.DiscoveryRequest{
   138  		VersionInfo: "",
   139  		Node: &v3corepb.Node{
   140  			Id:                   nodeID,
   141  			UserAgentName:        "user-agent",
   142  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"},
   143  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   144  		},
   145  		ResourceNames: []string{listenerName},
   146  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   147  		ResponseNonce: "",
   148  	}
   149  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   150  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   151  	}
   152  
   153  	// Capture the version and nonce from the response.
   154  	r, err = streamResponseCh.Receive(ctx)
   155  	if err != nil {
   156  		t.Fatal("Timeout when waiting for a discovery response from the server")
   157  	}
   158  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   159  
   160  	// Verify that the ACK contains the appropriate version and nonce.
   161  	r, err = streamRequestCh.Receive(ctx)
   162  	if err != nil {
   163  		t.Fatal("Timeout when waiting for ACK")
   164  	}
   165  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   166  	wantReq.VersionInfo = gotResp.GetVersionInfo()
   167  	wantReq.ResponseNonce = gotResp.GetNonce()
   168  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   169  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   170  	}
   171  
   172  	// Verify the update received by the watcher.
   173  	wantUpdate := listenerUpdateErrTuple{
   174  		update: listenerUpdate{RouteConfigName: routeConfigName},
   175  	}
   176  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   177  		t.Fatal(err)
   178  	}
   179  
   180  	// Update the management server with a listener resource that contains an
   181  	// empty HTTP connection manager within the apiListener, which will cause
   182  	// the resource to be NACKed.
   183  	badListener := proto.Clone(listenerResource).(*v3listenerpb.Listener)
   184  	badListener.ApiListener.ApiListener = nil
   185  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   186  		NodeID:         nodeID,
   187  		Listeners:      []*v3listenerpb.Listener{badListener},
   188  		SkipValidation: true,
   189  	})
   190  
   191  	r, err = streamResponseCh.Receive(ctx)
   192  	if err != nil {
   193  		t.Fatal("Timeout when waiting for a discovery response from the server")
   194  	}
   195  	gotResp = r.(*v3discoverypb.DiscoveryResponse)
   196  
   197  	wantNackErr := xdsresource.NewError(xdsresource.ErrorTypeNACKed, "unexpected http connection manager resource type")
   198  	if err := verifyListenerUpdate(ctx, lw.ambientErrCh, listenerUpdateErrTuple{ambientErr: wantNackErr}); err != nil {
   199  		t.Fatal(err)
   200  	}
   201  
   202  	// Verify that the NACK contains the appropriate version, nonce and error.
   203  	// We expect the version to not change as this is a NACK.
   204  	r, err = streamRequestCh.Receive(ctx)
   205  	if err != nil {
   206  		t.Fatal("Timeout when waiting for NACK")
   207  	}
   208  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   209  	if gotNonce, wantNonce := gotReq.GetResponseNonce(), gotResp.GetNonce(); gotNonce != wantNonce {
   210  		t.Errorf("Unexpected nonce in discovery request, got: %v, want: %v", gotNonce, wantNonce)
   211  	}
   212  	if gotErr := gotReq.GetErrorDetail(); gotErr == nil || !strings.Contains(gotErr.GetMessage(), wantNackErr.Error()) {
   213  		t.Fatalf("Unexpected error in discovery request, got: %v, want: %v", gotErr.GetMessage(), wantNackErr)
   214  	}
   215  
   216  	// Update the management server to send a good resource again.
   217  	mgmtServer.Update(ctx, e2e.UpdateOptions{
   218  		NodeID:         nodeID,
   219  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   220  		SkipValidation: true,
   221  	})
   222  
   223  	// The envoy-go-control-plane management server keeps resending the same
   224  	// resource as long as we keep NACK'ing it. So, we will see the bad resource
   225  	// sent to us a few times here, before receiving the good resource.
   226  	var lastErr error
   227  	for {
   228  		if ctx.Err() != nil {
   229  			t.Fatalf("Timeout when waiting for an ACK from the xDS client. Last seen error: %v", lastErr)
   230  		}
   231  
   232  		r, err = streamResponseCh.Receive(ctx)
   233  		if err != nil {
   234  			t.Fatal("Timeout when waiting for a discovery response from the server")
   235  		}
   236  		gotResp = r.(*v3discoverypb.DiscoveryResponse)
   237  
   238  		// Verify that the ACK contains the appropriate version and nonce.
   239  		r, err = streamRequestCh.Receive(ctx)
   240  		if err != nil {
   241  			t.Fatal("Timeout when waiting for ACK")
   242  		}
   243  		gotReq = r.(*v3discoverypb.DiscoveryRequest)
   244  		wantReq.VersionInfo = gotResp.GetVersionInfo()
   245  		wantReq.ResponseNonce = gotResp.GetNonce()
   246  		wantReq.ErrorDetail = nil
   247  		diff := cmp.Diff(gotReq, wantReq, protocmp.Transform())
   248  		if diff == "" {
   249  			lastErr = nil
   250  			break
   251  		}
   252  		lastErr = fmt.Errorf("unexpected diff in discovery request, diff (-got, +want):\n%s", diff)
   253  	}
   254  
   255  	// Verify the update received by the watcher.
   256  	for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) {
   257  		if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   258  			lastErr = err
   259  			continue
   260  		}
   261  		break
   262  	}
   263  	if ctx.Err() != nil {
   264  		t.Fatalf("Timeout when waiting for listener update. Last seen error: %v", lastErr)
   265  	}
   266  }
   267  
   268  // Tests the case where the first response is invalid. The test verifies that
   269  // the NACK contains an empty version string.
   270  func (s) TestADS_NACK_InvalidFirstResponse(t *testing.T) {
   271  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   272  	defer cancel()
   273  
   274  	// Create an xDS management server listening on a local port. Configure the
   275  	// request and response handlers to push on channels that are inspected by
   276  	// the test goroutine to verify ACK version and nonce.
   277  	streamRequestCh := testutils.NewChannelWithSize(1)
   278  	streamResponseCh := testutils.NewChannelWithSize(1)
   279  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   280  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   281  			streamRequestCh.SendContext(ctx, req)
   282  			return nil
   283  		},
   284  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   285  			streamResponseCh.SendContext(ctx, resp)
   286  		},
   287  	})
   288  
   289  	// Create a listener resource on the management server that is expected to
   290  	// be NACKed by the xDS client.
   291  	const listenerName = "listener"
   292  	const routeConfigName = "route-config"
   293  	nodeID := uuid.New().String()
   294  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
   295  	listenerResource.ApiListener.ApiListener = nil
   296  	resources := e2e.UpdateOptions{
   297  		NodeID:         nodeID,
   298  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   299  		SkipValidation: true,
   300  	}
   301  	if err := mgmtServer.Update(ctx, resources); err != nil {
   302  		t.Fatal(err)
   303  	}
   304  
   305  	// Create an xDS client pointing to the above server.
   306  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   307  	client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs))
   308  
   309  	// Register a watch for a listener resource.
   310  	lw := newListenerWatcher()
   311  	ldsCancel := client.WatchResource(xdsresource.V3ListenerURL, listenerName, lw)
   312  	defer ldsCancel()
   313  
   314  	// Verify that the initial discovery request matches expectation.
   315  	r, err := streamRequestCh.Receive(ctx)
   316  	if err != nil {
   317  		t.Fatal("Timeout when waiting for the initial discovery request")
   318  	}
   319  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   320  	wantReq := &v3discoverypb.DiscoveryRequest{
   321  		VersionInfo: "",
   322  		Node: &v3corepb.Node{
   323  			Id:                   nodeID,
   324  			UserAgentName:        "user-agent",
   325  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"},
   326  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   327  		},
   328  		ResourceNames: []string{listenerName},
   329  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   330  		ResponseNonce: "",
   331  	}
   332  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   333  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   334  	}
   335  
   336  	// Capture the version and nonce from the response.
   337  	r, err = streamResponseCh.Receive(ctx)
   338  	if err != nil {
   339  		t.Fatal("Timeout when waiting for the discovery response from client")
   340  	}
   341  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   342  
   343  	// Verify that the error is propagated to the watcher.
   344  	var wantNackErr = xdsresource.NewError(xdsresource.ErrorTypeNACKed, "unexpected http connection manager resource type")
   345  	if err := verifyListenerUpdate(ctx, lw.resourceErrCh, listenerUpdateErrTuple{resourceErr: wantNackErr}); err != nil {
   346  		t.Fatal(err)
   347  	}
   348  
   349  	// NACK should contain the appropriate error, nonce, but empty version.
   350  	r, err = streamRequestCh.Receive(ctx)
   351  	if err != nil {
   352  		t.Fatal("Timeout when waiting for ACK")
   353  	}
   354  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   355  	if gotVersion, wantVersion := gotReq.GetVersionInfo(), ""; gotVersion != wantVersion {
   356  		t.Errorf("Unexpected version in discovery request, got: %v, want: %v", gotVersion, wantVersion)
   357  	}
   358  	if gotNonce, wantNonce := gotReq.GetResponseNonce(), gotResp.GetNonce(); gotNonce != wantNonce {
   359  		t.Errorf("Unexpected nonce in discovery request, got: %v, want: %v", gotNonce, wantNonce)
   360  	}
   361  	if gotErr := gotReq.GetErrorDetail(); gotErr == nil || !strings.Contains(gotErr.GetMessage(), wantNackErr.Error()) {
   362  		t.Fatalf("Unexpected error in discovery request, got: %v, want: %v", gotErr.GetMessage(), wantNackErr)
   363  	}
   364  }
   365  
   366  // Tests the scenario where the xDS client is no longer interested in a
   367  // resource. The following sequence of events are tested:
   368  //  1. A resource is requested and a good response is received. The test verifies
   369  //     that an ACK is sent for this resource.
   370  //  2. The previously requested resource is no longer requested. The test
   371  //     verifies that the connection to the management server is closed.
   372  //  3. The same resource is requested again. The test verifies that a new
   373  //     request is sent with an empty version string, which corresponds to the
   374  //     first request on a new connection.
   375  func (s) TestADS_ACK_NACK_ResourceIsNotRequestedAnymore(t *testing.T) {
   376  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   377  	defer cancel()
   378  
   379  	// Create an xDS management server listening on a local port. Configure the
   380  	// request and response handlers to push on channels that are inspected by
   381  	// the test goroutine to verify ACK version and nonce.
   382  	streamRequestCh := testutils.NewChannelWithSize(1)
   383  	streamResponseCh := testutils.NewChannelWithSize(1)
   384  	streamCloseCh := testutils.NewChannelWithSize(1)
   385  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   386  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   387  			streamRequestCh.SendContext(ctx, req)
   388  			return nil
   389  		},
   390  		OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
   391  			streamResponseCh.SendContext(ctx, resp)
   392  		},
   393  		OnStreamClosed: func(int64, *v3corepb.Node) {
   394  			streamCloseCh.SendContext(ctx, struct{}{})
   395  		},
   396  	})
   397  
   398  	// Create a listener resource on the management server.
   399  	const listenerName = "listener"
   400  	const routeConfigName = "route-config"
   401  	nodeID := uuid.New().String()
   402  	listenerResource := e2e.DefaultClientListener(listenerName, routeConfigName)
   403  	resources := e2e.UpdateOptions{
   404  		NodeID:         nodeID,
   405  		Listeners:      []*v3listenerpb.Listener{listenerResource},
   406  		SkipValidation: true,
   407  	}
   408  	if err := mgmtServer.Update(ctx, resources); err != nil {
   409  		t.Fatal(err)
   410  	}
   411  
   412  	// Create an xDS client pointing to the above server.
   413  	configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}}
   414  	client := createXDSClient(t, mgmtServer.Address, nodeID, grpctransport.NewBuilder(configs))
   415  
   416  	// Register a watch for a listener resource.
   417  	lw := newListenerWatcher()
   418  	ldsCancel := client.WatchResource(xdsresource.V3ListenerURL, listenerName, lw)
   419  	defer ldsCancel()
   420  
   421  	// Verify that the initial discovery request matches expectation.
   422  	r, err := streamRequestCh.Receive(ctx)
   423  	if err != nil {
   424  		t.Fatal("Timeout when waiting for the initial discovery request")
   425  	}
   426  	gotReq := r.(*v3discoverypb.DiscoveryRequest)
   427  	wantReq := &v3discoverypb.DiscoveryRequest{
   428  		VersionInfo: "",
   429  		Node: &v3corepb.Node{
   430  			Id:                   nodeID,
   431  			UserAgentName:        "user-agent",
   432  			UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"},
   433  			ClientFeatures:       []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"},
   434  		},
   435  		ResourceNames: []string{listenerName},
   436  		TypeUrl:       "type.googleapis.com/envoy.config.listener.v3.Listener",
   437  		ResponseNonce: "",
   438  	}
   439  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   440  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   441  	}
   442  
   443  	// Capture the version and nonce from the response.
   444  	r, err = streamResponseCh.Receive(ctx)
   445  	if err != nil {
   446  		t.Fatal("Timeout when waiting for the discovery response from client")
   447  	}
   448  	gotResp := r.(*v3discoverypb.DiscoveryResponse)
   449  
   450  	// Verify that the ACK contains the appropriate version and nonce.
   451  	r, err = streamRequestCh.Receive(ctx)
   452  	if err != nil {
   453  		t.Fatal("Timeout when waiting for ACK")
   454  	}
   455  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   456  	wantACKReq := proto.Clone(wantReq).(*v3discoverypb.DiscoveryRequest)
   457  	wantACKReq.VersionInfo = gotResp.GetVersionInfo()
   458  	wantACKReq.ResponseNonce = gotResp.GetNonce()
   459  	if diff := cmp.Diff(gotReq, wantACKReq, protocmp.Transform()); diff != "" {
   460  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   461  	}
   462  
   463  	// Verify the update received by the watcher.
   464  	wantUpdate := listenerUpdateErrTuple{
   465  		update: listenerUpdate{RouteConfigName: routeConfigName},
   466  	}
   467  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   468  		t.Fatal(err)
   469  	}
   470  
   471  	// Cancel the watch on the listener resource. This should result in the
   472  	// existing connection to be management server getting closed.
   473  	ldsCancel()
   474  	if _, err := streamCloseCh.Receive(ctx); err != nil {
   475  		t.Fatalf("Timeout when expecting existing connection to be closed: %v", err)
   476  	}
   477  
   478  	// There is a race between two events when the last watch on an xdsChannel
   479  	// is canceled:
   480  	// - an empty discovery request being sent out
   481  	// - the ADS stream being closed
   482  	// To handle this race, we drain the request channel here so that if an
   483  	// empty discovery request was received, it is pulled out of the request
   484  	// channel and thereby guaranteeing a clean slate for the next watch
   485  	// registered below.
   486  	streamRequestCh.Drain()
   487  
   488  	// Register a watch for the same listener resource.
   489  	lw = newListenerWatcher()
   490  	ldsCancel = client.WatchResource(xdsresource.V3ListenerURL, listenerName, lw)
   491  	defer ldsCancel()
   492  
   493  	// Verify that the discovery request is identical to the first one sent out
   494  	// to the management server.
   495  	r, err = streamRequestCh.Receive(ctx)
   496  	if err != nil {
   497  		t.Fatal("Timeout when waiting for discovery request")
   498  	}
   499  	gotReq = r.(*v3discoverypb.DiscoveryRequest)
   500  	if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" {
   501  		t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff)
   502  	}
   503  
   504  	// Verify the update received by the watcher.
   505  	if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil {
   506  		t.Fatal(err)
   507  	}
   508  }