google.golang.org/grpc@v1.72.2/test/resolver_update_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 test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/balancer"
    32  	"google.golang.org/grpc/balancer/pickfirst"
    33  	"google.golang.org/grpc/codes"
    34  	"google.golang.org/grpc/credentials/insecure"
    35  	"google.golang.org/grpc/internal"
    36  	"google.golang.org/grpc/internal/balancer/stub"
    37  	"google.golang.org/grpc/internal/stubserver"
    38  	"google.golang.org/grpc/internal/testutils"
    39  	"google.golang.org/grpc/resolver"
    40  	"google.golang.org/grpc/resolver/manual"
    41  	"google.golang.org/grpc/serviceconfig"
    42  	"google.golang.org/grpc/status"
    43  
    44  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    45  	testpb "google.golang.org/grpc/interop/grpc_testing"
    46  )
    47  
    48  // TestResolverUpdateDuringBuild_ServiceConfigParseError makes the
    49  // resolver.Builder call into the ClientConn, during the Build call, with a
    50  // service config parsing error.
    51  //
    52  // We use two separate mutexes in the code which make sure there is no data race
    53  // in this code path, and also that there is no deadlock.
    54  func (s) TestResolverUpdateDuringBuild_ServiceConfigParseError(t *testing.T) {
    55  	// Setting InitialState on the manual resolver makes it call into the
    56  	// ClientConn during the Build call.
    57  	r := manual.NewBuilderWithScheme("whatever")
    58  	r.InitialState(resolver.State{ServiceConfig: &serviceconfig.ParseResult{Err: errors.New("resolver build err")}})
    59  
    60  	cc, err := grpc.NewClient(r.Scheme()+":///test.server", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
    61  	if err != nil {
    62  		t.Fatalf("NewClient(_, _) = _, %v; want _, nil", err)
    63  	}
    64  	defer cc.Close()
    65  
    66  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    67  	defer cancel()
    68  	client := testgrpc.NewTestServiceClient(cc)
    69  	const wantMsg = "error parsing service config"
    70  	const wantCode = codes.Unavailable
    71  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != wantCode || !strings.Contains(status.Convert(err).Message(), wantMsg) {
    72  		t.Fatalf("EmptyCall RPC failed: %v; want code: %v, want message: %q", err, wantCode, wantMsg)
    73  	}
    74  }
    75  
    76  type fakeConfig struct {
    77  	serviceconfig.Config
    78  }
    79  
    80  // TestResolverUpdateDuringBuild_ServiceConfigInvalidTypeError makes the
    81  // resolver.Builder call into the ClientConn, during the Build call, with an
    82  // invalid service config type.
    83  //
    84  // We use two separate mutexes in the code which make sure there is no data race
    85  // in this code path, and also that there is no deadlock.
    86  func (s) TestResolverUpdateDuringBuild_ServiceConfigInvalidTypeError(t *testing.T) {
    87  	// Setting InitialState on the manual resolver makes it call into the
    88  	// ClientConn during the Build call.
    89  	r := manual.NewBuilderWithScheme("whatever")
    90  	r.InitialState(resolver.State{ServiceConfig: &serviceconfig.ParseResult{Config: fakeConfig{}}})
    91  
    92  	cc, err := grpc.NewClient(r.Scheme()+":///test.server", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
    93  	if err != nil {
    94  		t.Fatalf("NewClient(_, _) = _, %v; want _, nil", err)
    95  	}
    96  	defer cc.Close()
    97  
    98  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    99  	defer cancel()
   100  	client := testgrpc.NewTestServiceClient(cc)
   101  	const wantMsg = "illegal service config type"
   102  	const wantCode = codes.Unavailable
   103  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != wantCode || !strings.Contains(status.Convert(err).Message(), wantMsg) {
   104  		t.Fatalf("EmptyCall RPC failed: %v; want code: %v, want message: %q", err, wantCode, wantMsg)
   105  	}
   106  }
   107  
   108  // TestResolverUpdate_InvalidServiceConfigAsFirstUpdate makes the resolver send
   109  // an update with an invalid service config as its first update. This should
   110  // make the ClientConn apply the failing LB policy, and should result in RPC
   111  // errors indicating the failing service config.
   112  func (s) TestResolverUpdate_InvalidServiceConfigAsFirstUpdate(t *testing.T) {
   113  	r := manual.NewBuilderWithScheme("whatever")
   114  
   115  	cc, err := grpc.NewClient(r.Scheme()+":///test.server", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
   116  	if err != nil {
   117  		t.Fatalf("NewClient(_, _) = _, %v; want _, nil", err)
   118  	}
   119  	cc.Connect()
   120  	defer cc.Close()
   121  
   122  	scpr := r.CC().ParseServiceConfig("bad json service config")
   123  	r.UpdateState(resolver.State{ServiceConfig: scpr})
   124  
   125  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   126  	defer cancel()
   127  	client := testgrpc.NewTestServiceClient(cc)
   128  	const wantMsg = "error parsing service config"
   129  	const wantCode = codes.Unavailable
   130  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != wantCode || !strings.Contains(status.Convert(err).Message(), wantMsg) {
   131  		t.Fatalf("EmptyCall RPC failed: %v; want code: %v, want message: %q", err, wantCode, wantMsg)
   132  	}
   133  }
   134  
   135  func verifyClientConnStateUpdate(got, want balancer.ClientConnState) error {
   136  	if got, want := got.ResolverState.Addresses, want.ResolverState.Addresses; !cmp.Equal(got, want) {
   137  		return fmt.Errorf("update got unexpected addresses: %v, want %v", got, want)
   138  	}
   139  	if got, want := got.ResolverState.ServiceConfig.Config, want.ResolverState.ServiceConfig.Config; !internal.EqualServiceConfigForTesting(got, want) {
   140  		return fmt.Errorf("received unexpected service config: \ngot: %v \nwant: %v", got, want)
   141  	}
   142  	if got, want := got.BalancerConfig, want.BalancerConfig; !cmp.Equal(got, want) {
   143  		return fmt.Errorf("received unexpected balancer config: \ngot: %v \nwant: %v", cmp.Diff(nil, got), cmp.Diff(nil, want))
   144  	}
   145  	return nil
   146  }
   147  
   148  // TestResolverUpdate_InvalidServiceConfigAfterGoodUpdate tests the scenario
   149  // where the resolver sends an update with an invalid service config after
   150  // having sent a good update. This should result in the ClientConn discarding
   151  // the new invalid service config, and continuing to use the old good config.
   152  func (s) TestResolverUpdate_InvalidServiceConfigAfterGoodUpdate(t *testing.T) {
   153  	type wrappingBalancerConfig struct {
   154  		serviceconfig.LoadBalancingConfig
   155  		Config string `json:"config,omitempty"`
   156  	}
   157  
   158  	// Register a stub balancer which uses a "pick_first" balancer underneath and
   159  	// signals on a channel when it receives ClientConn updates.
   160  	ccUpdateCh := testutils.NewChannel()
   161  	stub.Register(t.Name(), stub.BalancerFuncs{
   162  		Init: func(bd *stub.BalancerData) {
   163  			pf := balancer.Get(pickfirst.Name)
   164  			bd.Data = pf.Build(bd.ClientConn, bd.BuildOptions)
   165  		},
   166  		Close: func(bd *stub.BalancerData) {
   167  			bd.Data.(balancer.Balancer).Close()
   168  		},
   169  		ParseConfig: func(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
   170  			cfg := &wrappingBalancerConfig{}
   171  			if err := json.Unmarshal(lbCfg, cfg); err != nil {
   172  				return nil, err
   173  			}
   174  			return cfg, nil
   175  		},
   176  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   177  			if _, ok := ccs.BalancerConfig.(*wrappingBalancerConfig); !ok {
   178  				return fmt.Errorf("received balancer config of unsupported type %T", ccs.BalancerConfig)
   179  			}
   180  			bal := bd.Data.(balancer.Balancer)
   181  			ccUpdateCh.Send(ccs)
   182  			ccs.BalancerConfig = nil
   183  			return bal.UpdateClientConnState(ccs)
   184  		},
   185  	})
   186  
   187  	// Start a backend exposing the test service.
   188  	backend := &stubserver.StubServer{
   189  		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) { return &testpb.Empty{}, nil },
   190  	}
   191  	if err := backend.StartServer(); err != nil {
   192  		t.Fatalf("Failed to start backend: %v", err)
   193  	}
   194  	t.Logf("Started TestService backend at: %q", backend.Address)
   195  	defer backend.Stop()
   196  
   197  	r := manual.NewBuilderWithScheme("whatever")
   198  
   199  	cc, err := grpc.NewClient(r.Scheme()+":///test.server", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
   200  	if err != nil {
   201  		t.Fatalf("NewClient(_, _) = _, %v; want _, nil", err)
   202  	}
   203  	defer cc.Close()
   204  	cc.Connect()
   205  	// Push a resolver update and verify that our balancer receives the update.
   206  	addrs := []resolver.Address{{Addr: backend.Address}}
   207  	const lbCfg = "wrapping balancer LB policy config"
   208  	goodSC := r.CC().ParseServiceConfig(fmt.Sprintf(`
   209  {
   210    "loadBalancingConfig": [
   211      {
   212        "%v": {
   213          "config": "%s"
   214        }
   215      }
   216    ]
   217  }`, t.Name(), lbCfg))
   218  	r.UpdateState(resolver.State{Addresses: addrs, ServiceConfig: goodSC})
   219  
   220  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   221  	defer cancel()
   222  	wantCCS := balancer.ClientConnState{
   223  		ResolverState: resolver.State{
   224  			Addresses:     addrs,
   225  			ServiceConfig: goodSC,
   226  		},
   227  		BalancerConfig: &wrappingBalancerConfig{Config: lbCfg},
   228  	}
   229  	ccs, err := ccUpdateCh.Receive(ctx)
   230  	if err != nil {
   231  		t.Fatalf("Timeout when waiting for ClientConnState update from grpc")
   232  	}
   233  	gotCCS := ccs.(balancer.ClientConnState)
   234  	if err := verifyClientConnStateUpdate(gotCCS, wantCCS); err != nil {
   235  		t.Fatal(err)
   236  	}
   237  
   238  	// Ensure RPCs are successful.
   239  	client := testgrpc.NewTestServiceClient(cc)
   240  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   241  		t.Fatalf("EmptyCall RPC failed: %v", err)
   242  	}
   243  
   244  	// Push a bad resolver update and ensure that the update is propagated to our
   245  	// stub balancer. But since the pushed update contains an invalid service
   246  	// config, our balancer should continue to see the old loadBalancingConfig.
   247  	badSC := r.CC().ParseServiceConfig("bad json service config")
   248  	wantCCS.ResolverState.ServiceConfig = badSC
   249  	r.UpdateState(resolver.State{Addresses: addrs, ServiceConfig: badSC})
   250  	ccs, err = ccUpdateCh.Receive(ctx)
   251  	if err != nil {
   252  		t.Fatalf("Timeout when waiting for ClientConnState update from grpc")
   253  	}
   254  	gotCCS = ccs.(balancer.ClientConnState)
   255  	if err := verifyClientConnStateUpdate(gotCCS, wantCCS); err != nil {
   256  		t.Fatal(err)
   257  	}
   258  
   259  	// RPCs should continue to be successful since the ClientConn is using the old
   260  	// good service config.
   261  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   262  		t.Fatalf("EmptyCall RPC failed: %v", err)
   263  	}
   264  }