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