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  }