google.golang.org/grpc@v1.72.2/test/xds/xds_client_ack_nack_test.go (about)

     1  /*
     2   *
     3   * Copyright 2022 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 xds_test
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"testing"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/google/uuid"
    28  	"google.golang.org/grpc"
    29  	"google.golang.org/grpc/credentials/insecure"
    30  	"google.golang.org/grpc/internal"
    31  	"google.golang.org/grpc/internal/grpcsync"
    32  	"google.golang.org/grpc/internal/stubserver"
    33  	"google.golang.org/grpc/internal/testutils"
    34  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    35  	"google.golang.org/grpc/resolver"
    36  
    37  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    38  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    39  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    40  	testpb "google.golang.org/grpc/interop/grpc_testing"
    41  )
    42  
    43  // We are interested in LDS, RDS, CDS and EDS resources as part of the regular
    44  // xDS flow on the client.
    45  const wantResources = 4
    46  
    47  // seenAllACKs returns true if the provided ackVersions map contains valid acks
    48  // for all the resources that we are interested in. If `wantNonEmpty` is true,
    49  // only non-empty ack versions are considered valid.
    50  func seenAllACKs(acksVersions map[string]string, wantNonEmpty bool) bool {
    51  	if len(acksVersions) != wantResources {
    52  		return false
    53  	}
    54  	for _, ack := range acksVersions {
    55  		if wantNonEmpty && ack == "" {
    56  			return false
    57  		}
    58  	}
    59  	return true
    60  }
    61  
    62  // TestClientResourceVersionAfterStreamRestart tests the scenario where the
    63  // xdsClient's ADS stream to the management server gets broken. This test
    64  // verifies that the version number on the initial request on the new stream
    65  // indicates the most recent version seen by the client on the previous stream.
    66  func (s) TestClientResourceVersionAfterStreamRestart(t *testing.T) {
    67  	// Create a restartable listener which can close existing connections.
    68  	l, err := testutils.LocalTCPListener()
    69  	if err != nil {
    70  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
    71  	}
    72  	lis := testutils.NewRestartableListener(l)
    73  
    74  	// We depend on the fact that the management server assigns monotonically
    75  	// increasing stream IDs starting at 1.
    76  	const (
    77  		idBeforeRestart = 1
    78  		idAfterRestart  = 2
    79  	)
    80  
    81  	// Events of importance in the test, in the order in which they are expected
    82  	// to happen.
    83  	acksReceivedBeforeRestart := grpcsync.NewEvent()
    84  	streamRestarted := grpcsync.NewEvent()
    85  	acksReceivedAfterRestart := grpcsync.NewEvent()
    86  
    87  	// Map from stream id to a map of resource type to resource version.
    88  	ackVersionsMap := make(map[int64]map[string]string)
    89  	managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
    90  		Listener: lis,
    91  		OnStreamRequest: func(id int64, req *v3discoverypb.DiscoveryRequest) error {
    92  			// Return early under the following circumstances:
    93  			// - Received all the requests we wanted to see. This is to avoid
    94  			//   any stray requests leading to test flakes.
    95  			// - Request contains no resource names. Such requests are usually
    96  			//   seen when the xdsclient is shutting down and is no longer
    97  			//   interested in the resources that it had subscribed to earlier.
    98  			if acksReceivedAfterRestart.HasFired() || len(req.GetResourceNames()) == 0 {
    99  				return nil
   100  			}
   101  			// Create a stream specific map to store ack versions if this is the
   102  			// first time we are seeing this stream id.
   103  			if ackVersionsMap[id] == nil {
   104  				ackVersionsMap[id] = make(map[string]string)
   105  			}
   106  			ackVersionsMap[id][req.GetTypeUrl()] = req.GetVersionInfo()
   107  			// Prior to stream restart, we are interested only in non-empty
   108  			// resource versions. The xdsclient first sends out requests with an
   109  			// empty version string. After receipt of requested resource, it
   110  			// sends out another request for the same resource, but this time
   111  			// with a non-empty version string, to serve as an ACK.
   112  			if seenAllACKs(ackVersionsMap[idBeforeRestart], true) {
   113  				acksReceivedBeforeRestart.Fire()
   114  			}
   115  			// After stream restart, we expect the xdsclient to send out
   116  			// requests with version string set to the previously ACKed
   117  			// versions. If it sends out requests with empty version string, it
   118  			// is a bug and we want this test to catch it.
   119  			if seenAllACKs(ackVersionsMap[idAfterRestart], false) {
   120  				acksReceivedAfterRestart.Fire()
   121  			}
   122  			return nil
   123  		},
   124  		OnStreamClosed: func(int64, *v3corepb.Node) {
   125  			streamRestarted.Fire()
   126  		},
   127  	})
   128  
   129  	// Create bootstrap configuration pointing to the above management server.
   130  	nodeID := uuid.New().String()
   131  	bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
   132  
   133  	// Create an xDS resolver with the above bootstrap configuration.
   134  	if internal.NewXDSResolverWithConfigForTesting == nil {
   135  		t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil")
   136  	}
   137  	xdsResolver, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
   138  	if err != nil {
   139  		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
   140  	}
   141  
   142  	server := stubserver.StartTestService(t, nil)
   143  	defer server.Stop()
   144  
   145  	const serviceName = "my-service-client-side-xds"
   146  	resources := e2e.DefaultClientResources(e2e.ResourceParams{
   147  		DialTarget: serviceName,
   148  		NodeID:     nodeID,
   149  		Host:       "localhost",
   150  		Port:       testutils.ParsePort(t, server.Address),
   151  		SecLevel:   e2e.SecurityLevelNone,
   152  	})
   153  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   154  	defer cancel()
   155  	if err := managementServer.Update(ctx, resources); err != nil {
   156  		t.Fatal(err)
   157  	}
   158  
   159  	// Create a ClientConn and make a successful RPC.
   160  	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   161  	if err != nil {
   162  		t.Fatalf("failed to dial local test server: %v", err)
   163  	}
   164  	defer cc.Close()
   165  
   166  	client := testgrpc.NewTestServiceClient(cc)
   167  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   168  		t.Fatalf("rpc EmptyCall() failed: %v", err)
   169  	}
   170  
   171  	// A successful RPC means that the xdsclient received all requested
   172  	// resources. The ACKs from the xdsclient may get a little delayed. So, we
   173  	// need to wait for all ACKs to be received on the management server before
   174  	// restarting the stream.
   175  	select {
   176  	case <-ctx.Done():
   177  		t.Fatal("Timeout when waiting for all resources to be ACKed prior to stream restart")
   178  	case <-acksReceivedBeforeRestart.Done():
   179  	}
   180  
   181  	// Stop the listener on the management server. This will cause the client to
   182  	// backoff and recreate the stream.
   183  	lis.Stop()
   184  
   185  	// Wait for the stream to be closed on the server.
   186  	<-streamRestarted.Done()
   187  
   188  	// Restart the listener on the management server to be able to accept
   189  	// reconnect attempts from the client.
   190  	lis.Restart()
   191  
   192  	// Wait for all the previously sent resources to be re-requested.
   193  	select {
   194  	case <-ctx.Done():
   195  		t.Fatal("Timeout when waiting for all resources to be ACKed post stream restart")
   196  	case <-acksReceivedAfterRestart.Done():
   197  	}
   198  
   199  	if diff := cmp.Diff(ackVersionsMap[idBeforeRestart], ackVersionsMap[idAfterRestart]); diff != "" {
   200  		t.Fatalf("unexpected diff in ack versions before and after stream restart (-want, +got):\n%s", diff)
   201  	}
   202  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   203  		t.Fatalf("rpc EmptyCall() failed: %v", err)
   204  	}
   205  }