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