google.golang.org/grpc@v1.72.2/balancer/endpointsharding/endpointsharding_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 endpointsharding_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"log"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/grpc/backoff"
    33  	"google.golang.org/grpc/balancer"
    34  	"google.golang.org/grpc/balancer/endpointsharding"
    35  	"google.golang.org/grpc/balancer/pickfirst/pickfirstleaf"
    36  	"google.golang.org/grpc/codes"
    37  	"google.golang.org/grpc/connectivity"
    38  	"google.golang.org/grpc/credentials/insecure"
    39  	"google.golang.org/grpc/grpclog"
    40  	"google.golang.org/grpc/internal"
    41  	"google.golang.org/grpc/internal/balancer/stub"
    42  	"google.golang.org/grpc/internal/grpctest"
    43  	"google.golang.org/grpc/internal/stubserver"
    44  	"google.golang.org/grpc/internal/testutils"
    45  	"google.golang.org/grpc/internal/testutils/roundrobin"
    46  	"google.golang.org/grpc/peer"
    47  	"google.golang.org/grpc/resolver"
    48  	"google.golang.org/grpc/resolver/manual"
    49  	"google.golang.org/grpc/serviceconfig"
    50  	"google.golang.org/grpc/status"
    51  
    52  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    53  	testpb "google.golang.org/grpc/interop/grpc_testing"
    54  )
    55  
    56  var (
    57  	defaultTestTimeout      = time.Second * 10
    58  	defaultTestShortTimeout = time.Millisecond * 10
    59  )
    60  
    61  type s struct {
    62  	grpctest.Tester
    63  }
    64  
    65  func Test(t *testing.T) {
    66  	grpctest.RunSubTests(t, s{})
    67  }
    68  
    69  var logger = grpclog.Component("endpoint-sharding-test")
    70  
    71  func init() {
    72  	balancer.Register(fakePetioleBuilder{})
    73  }
    74  
    75  const fakePetioleName = "fake_petiole"
    76  
    77  type fakePetioleBuilder struct{}
    78  
    79  func (fakePetioleBuilder) Name() string {
    80  	return fakePetioleName
    81  }
    82  
    83  func (fakePetioleBuilder) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
    84  	fp := &fakePetiole{
    85  		ClientConn: cc,
    86  		bOpts:      opts,
    87  	}
    88  	fp.Balancer = endpointsharding.NewBalancer(fp, opts, balancer.Get(pickfirstleaf.Name).Build, endpointsharding.Options{})
    89  	return fp
    90  }
    91  
    92  func (fakePetioleBuilder) ParseConfig(json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
    93  	return nil, nil
    94  }
    95  
    96  // fakePetiole is a load balancer that wraps the endpointShardingBalancer, and
    97  // forwards ClientConnUpdates with a child config of graceful switch that wraps
    98  // pick first. It also intercepts UpdateState to make sure it can access the
    99  // child state maintained by EndpointSharding.
   100  type fakePetiole struct {
   101  	balancer.Balancer
   102  	balancer.ClientConn
   103  	bOpts balancer.BuildOptions
   104  }
   105  
   106  func (fp *fakePetiole) UpdateClientConnState(state balancer.ClientConnState) error {
   107  	if el := state.ResolverState.Endpoints; len(el) != 2 {
   108  		return fmt.Errorf("UpdateClientConnState wants two endpoints, got: %v", el)
   109  	}
   110  
   111  	return fp.Balancer.UpdateClientConnState(state)
   112  }
   113  
   114  func (fp *fakePetiole) UpdateState(state balancer.State) {
   115  	childStates := endpointsharding.ChildStatesFromPicker(state.Picker)
   116  	// Both child states should be present in the child picker. States and
   117  	// picker change over the lifecycle of test, but there should always be two.
   118  	if len(childStates) != 2 {
   119  		logger.Fatal(fmt.Errorf("length of child states received: %v, want 2", len(childStates)))
   120  	}
   121  
   122  	fp.ClientConn.UpdateState(state)
   123  }
   124  
   125  // TestEndpointShardingBasic tests the basic functionality of the endpoint
   126  // sharding balancer. It specifies a petiole policy that is essentially a
   127  // wrapper around the endpoint sharder. Two backends are started, with each
   128  // backend's address specified in an endpoint. The petiole does not have a
   129  // special picker, so it should fallback to the default behavior, which is to
   130  // round_robin amongst the endpoint children that are in the aggregated state.
   131  // It also verifies the petiole has access to the raw child state in case it
   132  // wants to implement a custom picker. The test sends a resolver error to the
   133  // endpointsharding balancer and verifies an error picker from the children
   134  // is used while making an RPC.
   135  func (s) TestEndpointShardingBasic(t *testing.T) {
   136  	backend1 := stubserver.StartTestService(t, nil)
   137  	defer backend1.Stop()
   138  	backend2 := stubserver.StartTestService(t, nil)
   139  	defer backend2.Stop()
   140  
   141  	mr := manual.NewBuilderWithScheme("e2e-test")
   142  	defer mr.Close()
   143  
   144  	json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, fakePetioleName)
   145  	sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
   146  	mr.InitialState(resolver.State{
   147  		Endpoints: []resolver.Endpoint{
   148  			{Addresses: []resolver.Address{{Addr: backend1.Address}}},
   149  			{Addresses: []resolver.Address{{Addr: backend2.Address}}},
   150  		},
   151  		ServiceConfig: sc,
   152  	})
   153  
   154  	dOpts := []grpc.DialOption{
   155  		grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()),
   156  		// Use a large backoff delay to avoid the error picker being updated
   157  		// too quickly.
   158  		grpc.WithConnectParams(grpc.ConnectParams{
   159  			Backoff: backoff.Config{
   160  				BaseDelay:  2 * defaultTestTimeout,
   161  				Multiplier: float64(0),
   162  				Jitter:     float64(0),
   163  				MaxDelay:   2 * defaultTestTimeout,
   164  			},
   165  		}),
   166  	}
   167  	cc, err := grpc.NewClient(mr.Scheme()+":///", dOpts...)
   168  	if err != nil {
   169  		log.Fatalf("Failed to create new client: %v", err)
   170  	}
   171  	defer cc.Close()
   172  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   173  	defer cancel()
   174  	client := testgrpc.NewTestServiceClient(cc)
   175  	// Assert a round robin distribution between the two spun up backends. This
   176  	// requires a poll and eventual consistency as both endpoint children do not
   177  	// start in state READY.
   178  	if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend1.Address}, {Addr: backend2.Address}}); err != nil {
   179  		t.Fatalf("error in expected round robin: %v", err)
   180  	}
   181  
   182  	// Stopping both the backends should make the channel enter
   183  	// TransientFailure.
   184  	backend1.Stop()
   185  	backend2.Stop()
   186  	testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
   187  
   188  	// When the resolver reports an error, the picker should get updated to
   189  	// return the resolver error.
   190  	mr.CC().ReportError(errors.New("test error"))
   191  	testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
   192  	for ; ctx.Err() == nil; <-time.After(time.Millisecond) {
   193  		_, err := client.EmptyCall(ctx, &testpb.Empty{})
   194  		if err == nil {
   195  			t.Fatalf("EmptyCall succeeded when expected to fail with %q", "test error")
   196  		}
   197  		if strings.Contains(err.Error(), "test error") {
   198  			break
   199  		}
   200  	}
   201  	if ctx.Err() != nil {
   202  		t.Fatalf("Context timed out waiting for picker with resolver error.")
   203  	}
   204  }
   205  
   206  // Tests that endpointsharding doesn't automatically re-connect IDLE children.
   207  // The test creates an endpoint with two servers and another with a single
   208  // server. The active service in endpoint 1 is closed to make the child
   209  // pickfirst enter IDLE state. The test verifies that the child pickfirst
   210  // doesn't connect to the second address in the endpoint.
   211  func (s) TestEndpointShardingReconnectDisabled(t *testing.T) {
   212  	backend1 := stubserver.StartTestService(t, nil)
   213  	defer backend1.Stop()
   214  	backend2 := stubserver.StartTestService(t, nil)
   215  	defer backend2.Stop()
   216  	backend3 := stubserver.StartTestService(t, nil)
   217  	defer backend3.Stop()
   218  
   219  	mr := manual.NewBuilderWithScheme("e2e-test")
   220  	defer mr.Close()
   221  
   222  	name := strings.ReplaceAll(strings.ToLower(t.Name()), "/", "")
   223  	bf := stub.BalancerFuncs{
   224  		Init: func(bd *stub.BalancerData) {
   225  			epOpts := endpointsharding.Options{DisableAutoReconnect: true}
   226  			bd.Data = endpointsharding.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirstleaf.Name).Build, epOpts)
   227  		},
   228  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   229  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   230  		},
   231  		Close: func(bd *stub.BalancerData) {
   232  			bd.Data.(balancer.Balancer).Close()
   233  		},
   234  	}
   235  	stub.Register(name, bf)
   236  
   237  	json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, name)
   238  	sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
   239  	mr.InitialState(resolver.State{
   240  		Endpoints: []resolver.Endpoint{
   241  			{Addresses: []resolver.Address{{Addr: backend1.Address}, {Addr: backend2.Address}}},
   242  			{Addresses: []resolver.Address{{Addr: backend3.Address}}},
   243  		},
   244  		ServiceConfig: sc,
   245  	})
   246  
   247  	cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
   248  	if err != nil {
   249  		log.Fatalf("Failed to create new client: %v", err)
   250  	}
   251  	defer cc.Close()
   252  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   253  	defer cancel()
   254  	client := testgrpc.NewTestServiceClient(cc)
   255  	// Assert a round robin distribution between the two spun up backends. This
   256  	// requires a poll and eventual consistency as both endpoint children do not
   257  	// start in state READY.
   258  	if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend1.Address}, {Addr: backend3.Address}}); err != nil {
   259  		t.Fatalf("error in expected round robin: %v", err)
   260  	}
   261  
   262  	// On closing the first server, the first child balancer should enter
   263  	// IDLE. Since endpointsharding is configured not to auto-reconnect, it will
   264  	// remain IDLE and will not try to connect to the second backend in the same
   265  	// endpoint.
   266  	backend1.Stop()
   267  	// CheckRoundRobinRPCs waits for all the backends to become reachable, we
   268  	// call it to ensure the picker no longer sends RPCs to closed backend.
   269  	if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend3.Address}}); err != nil {
   270  		t.Fatalf("error in expected round robin: %v", err)
   271  	}
   272  
   273  	// Verify requests go only to backend3 for a short time.
   274  	shortCtx, cancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   275  	defer cancel()
   276  	for ; shortCtx.Err() == nil; <-time.After(time.Millisecond) {
   277  		var peer peer.Peer
   278  		if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
   279  			if status.Code(err) != codes.DeadlineExceeded {
   280  				t.Fatalf("EmptyCall() returned unexpected error %v", err)
   281  			}
   282  			break
   283  		}
   284  		if got, want := peer.Addr.String(), backend3.Address; got != want {
   285  			t.Fatalf("EmptyCall() went to unexpected backend: got %q, want %q", got, want)
   286  		}
   287  	}
   288  }