mirror of https://github.com/grpc/grpc-go.git
206 lines
7.6 KiB
Go
206 lines
7.6 KiB
Go
/*
|
|
*
|
|
* Copyright 2022 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package xds_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/uuid"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/internal"
|
|
"google.golang.org/grpc/internal/grpcsync"
|
|
"google.golang.org/grpc/internal/stubserver"
|
|
"google.golang.org/grpc/internal/testutils"
|
|
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
|
"google.golang.org/grpc/resolver"
|
|
|
|
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
|
|
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
|
testpb "google.golang.org/grpc/interop/grpc_testing"
|
|
)
|
|
|
|
// We are interested in LDS, RDS, CDS and EDS resources as part of the regular
|
|
// xDS flow on the client.
|
|
const wantResources = 4
|
|
|
|
// seenAllACKs returns true if the provided ackVersions map contains valid acks
|
|
// for all the resources that we are interested in. If `wantNonEmpty` is true,
|
|
// only non-empty ack versions are considered valid.
|
|
func seenAllACKs(acksVersions map[string]string, wantNonEmpty bool) bool {
|
|
if len(acksVersions) != wantResources {
|
|
return false
|
|
}
|
|
for _, ack := range acksVersions {
|
|
if wantNonEmpty && ack == "" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TestClientResourceVersionAfterStreamRestart tests the scenario where the
|
|
// xdsClient's ADS stream to the management server gets broken. This test
|
|
// verifies that the version number on the initial request on the new stream
|
|
// indicates the most recent version seen by the client on the previous stream.
|
|
func (s) TestClientResourceVersionAfterStreamRestart(t *testing.T) {
|
|
// Create a restartable listener which can close existing connections.
|
|
l, err := testutils.LocalTCPListener()
|
|
if err != nil {
|
|
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
|
}
|
|
lis := testutils.NewRestartableListener(l)
|
|
|
|
// We depend on the fact that the management server assigns monotonically
|
|
// increasing stream IDs starting at 1.
|
|
const (
|
|
idBeforeRestart = 1
|
|
idAfterRestart = 2
|
|
)
|
|
|
|
// Events of importance in the test, in the order in which they are expected
|
|
// to happen.
|
|
acksReceivedBeforeRestart := grpcsync.NewEvent()
|
|
streamRestarted := grpcsync.NewEvent()
|
|
acksReceivedAfterRestart := grpcsync.NewEvent()
|
|
|
|
// Map from stream id to a map of resource type to resource version.
|
|
ackVersionsMap := make(map[int64]map[string]string)
|
|
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
|
Listener: lis,
|
|
OnStreamRequest: func(id int64, req *v3discoverypb.DiscoveryRequest) error {
|
|
// Return early under the following circumstances:
|
|
// - Received all the requests we wanted to see. This is to avoid
|
|
// any stray requests leading to test flakes.
|
|
// - Request contains no resource names. Such requests are usually
|
|
// seen when the xdsclient is shutting down and is no longer
|
|
// interested in the resources that it had subscribed to earlier.
|
|
if acksReceivedAfterRestart.HasFired() || len(req.GetResourceNames()) == 0 {
|
|
return nil
|
|
}
|
|
// Create a stream specific map to store ack versions if this is the
|
|
// first time we are seeing this stream id.
|
|
if ackVersionsMap[id] == nil {
|
|
ackVersionsMap[id] = make(map[string]string)
|
|
}
|
|
ackVersionsMap[id][req.GetTypeUrl()] = req.GetVersionInfo()
|
|
// Prior to stream restart, we are interested only in non-empty
|
|
// resource versions. The xdsclient first sends out requests with an
|
|
// empty version string. After receipt of requested resource, it
|
|
// sends out another request for the same resource, but this time
|
|
// with a non-empty version string, to serve as an ACK.
|
|
if seenAllACKs(ackVersionsMap[idBeforeRestart], true) {
|
|
acksReceivedBeforeRestart.Fire()
|
|
}
|
|
// After stream restart, we expect the xdsclient to send out
|
|
// requests with version string set to the previously ACKed
|
|
// versions. If it sends out requests with empty version string, it
|
|
// is a bug and we want this test to catch it.
|
|
if seenAllACKs(ackVersionsMap[idAfterRestart], false) {
|
|
acksReceivedAfterRestart.Fire()
|
|
}
|
|
return nil
|
|
},
|
|
OnStreamClosed: func(int64, *v3corepb.Node) {
|
|
streamRestarted.Fire()
|
|
},
|
|
})
|
|
|
|
// Create bootstrap configuration pointing to the above management server.
|
|
nodeID := uuid.New().String()
|
|
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
|
|
|
// Create an xDS resolver with the above bootstrap configuration.
|
|
var xdsResolver resolver.Builder
|
|
if newResolver := internal.NewXDSResolverWithConfigForTesting; newResolver != nil {
|
|
xdsResolver, err = newResolver.(func([]byte) (resolver.Builder, error))(bootstrapContents)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
|
|
}
|
|
}
|
|
|
|
server := stubserver.StartTestService(t, nil)
|
|
defer server.Stop()
|
|
|
|
const serviceName = "my-service-client-side-xds"
|
|
resources := e2e.DefaultClientResources(e2e.ResourceParams{
|
|
DialTarget: serviceName,
|
|
NodeID: nodeID,
|
|
Host: "localhost",
|
|
Port: testutils.ParsePort(t, server.Address),
|
|
SecLevel: e2e.SecurityLevelNone,
|
|
})
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
if err := managementServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create a ClientConn and make a successful RPC.
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
|
t.Fatalf("rpc EmptyCall() failed: %v", err)
|
|
}
|
|
|
|
// A successful RPC means that the xdsclient received all requested
|
|
// resources. The ACKs from the xdsclient may get a little delayed. So, we
|
|
// need to wait for all ACKs to be received on the management server before
|
|
// restarting the stream.
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("Timeout when waiting for all resources to be ACKed prior to stream restart")
|
|
case <-acksReceivedBeforeRestart.Done():
|
|
}
|
|
|
|
// Stop the listener on the management server. This will cause the client to
|
|
// backoff and recreate the stream.
|
|
lis.Stop()
|
|
|
|
// Wait for the stream to be closed on the server.
|
|
<-streamRestarted.Done()
|
|
|
|
// Restart the listener on the management server to be able to accept
|
|
// reconnect attempts from the client.
|
|
lis.Restart()
|
|
|
|
// Wait for all the previously sent resources to be re-requested.
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("Timeout when waiting for all resources to be ACKed post stream restart")
|
|
case <-acksReceivedAfterRestart.Done():
|
|
}
|
|
|
|
if diff := cmp.Diff(ackVersionsMap[idBeforeRestart], ackVersionsMap[idAfterRestart]); diff != "" {
|
|
t.Fatalf("unexpected diff in ack versions before and after stream restart (-want, +got):\n%s", diff)
|
|
}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
|
t.Fatalf("rpc EmptyCall() failed: %v", err)
|
|
}
|
|
}
|