google.golang.org/grpc@v1.72.2/balancer/lazy/lazy_ext_test.go (about)

     1  /*
     2   *
     3   * Copyright 2025 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 lazy_test
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"google.golang.org/grpc"
    30  	"google.golang.org/grpc/balancer"
    31  	"google.golang.org/grpc/balancer/lazy"
    32  	"google.golang.org/grpc/balancer/pickfirst/pickfirstleaf"
    33  	"google.golang.org/grpc/connectivity"
    34  	"google.golang.org/grpc/credentials/insecure"
    35  	"google.golang.org/grpc/internal/balancer/stub"
    36  	"google.golang.org/grpc/internal/grpcsync"
    37  	"google.golang.org/grpc/internal/grpctest"
    38  	"google.golang.org/grpc/internal/stubserver"
    39  	"google.golang.org/grpc/internal/testutils"
    40  	"google.golang.org/grpc/peer"
    41  	"google.golang.org/grpc/resolver"
    42  	"google.golang.org/grpc/resolver/manual"
    43  
    44  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    45  	testpb "google.golang.org/grpc/interop/grpc_testing"
    46  )
    47  
    48  const (
    49  	// Default timeout for tests in this package.
    50  	defaultTestTimeout = 10 * time.Second
    51  	// Default short timeout, to be used when waiting for events which are not
    52  	// expected to happen.
    53  	defaultTestShortTimeout = 100 * time.Millisecond
    54  )
    55  
    56  type s struct {
    57  	grpctest.Tester
    58  }
    59  
    60  func Test(t *testing.T) {
    61  	grpctest.RunSubTests(t, s{})
    62  }
    63  
    64  // TestExitIdle creates a lazy balancer than manages a pickfirst child. The test
    65  // calls Connect() on the channel which in turn calls ExitIdle on the lazy
    66  // balancer. The test verifies that the channel enters READY.
    67  func (s) TestExitIdle(t *testing.T) {
    68  	backend1 := stubserver.StartTestService(t, nil)
    69  	defer backend1.Stop()
    70  
    71  	mr := manual.NewBuilderWithScheme("e2e-test")
    72  	defer mr.Close()
    73  
    74  	mr.InitialState(resolver.State{
    75  		Endpoints: []resolver.Endpoint{
    76  			{Addresses: []resolver.Address{{Addr: backend1.Address}}},
    77  		},
    78  	})
    79  
    80  	bf := stub.BalancerFuncs{
    81  		Init: func(bd *stub.BalancerData) {
    82  			bd.Data = lazy.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirstleaf.Name).Build)
    83  		},
    84  		ExitIdle: func(bd *stub.BalancerData) {
    85  			bd.Data.(balancer.ExitIdler).ExitIdle()
    86  		},
    87  		ResolverError: func(bd *stub.BalancerData, err error) {
    88  			bd.Data.(balancer.Balancer).ResolverError(err)
    89  		},
    90  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
    91  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
    92  		},
    93  		Close: func(bd *stub.BalancerData) {
    94  			bd.Data.(balancer.Balancer).Close()
    95  		},
    96  	}
    97  	stub.Register(t.Name(), bf)
    98  	json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s": {}}]}`, t.Name())
    99  	opts := []grpc.DialOption{
   100  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   101  		grpc.WithDefaultServiceConfig(json),
   102  		grpc.WithResolvers(mr),
   103  	}
   104  	cc, err := grpc.NewClient(mr.Scheme()+":///", opts...)
   105  	if err != nil {
   106  		t.Fatalf("grpc.NewClient(_) failed: %v", err)
   107  	}
   108  	defer cc.Close()
   109  
   110  	cc.Connect()
   111  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   112  	defer cancel()
   113  	testutils.AwaitState(ctx, t, cc, connectivity.Ready)
   114  
   115  	// Send a resolver update to verify that the resolver state is correctly
   116  	// passed through to the leaf pickfirst balancer.
   117  	backend2 := stubserver.StartTestService(t, nil)
   118  	defer backend2.Stop()
   119  
   120  	mr.UpdateState(resolver.State{
   121  		Endpoints: []resolver.Endpoint{
   122  			{Addresses: []resolver.Address{{Addr: backend2.Address}}},
   123  		},
   124  	})
   125  
   126  	var peer peer.Peer
   127  	client := testgrpc.NewTestServiceClient(cc)
   128  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
   129  		t.Errorf("client.EmptyCall() returned unexpected error: %v", err)
   130  	}
   131  	if got, want := peer.Addr.String(), backend2.Address; got != want {
   132  		t.Errorf("EmptyCall() went to unexpected backend: got %q, want %q", got, want)
   133  	}
   134  }
   135  
   136  // TestPicker creates a lazy balancer under a stub balancer which block all
   137  // calls to ExitIdle. This ensures the only way to trigger lazy to exit idle is
   138  // through the picker. The test makes an RPC and ensures it succeeds.
   139  func (s) TestPicker(t *testing.T) {
   140  	backend := stubserver.StartTestService(t, nil)
   141  	defer backend.Stop()
   142  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   143  	defer cancel()
   144  
   145  	bf := stub.BalancerFuncs{
   146  		Init: func(bd *stub.BalancerData) {
   147  			bd.Data = lazy.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirstleaf.Name).Build)
   148  		},
   149  		ExitIdle: func(bd *stub.BalancerData) {
   150  			t.Log("Ignoring call to ExitIdle, calling the picker should make the lazy balancer exit IDLE state.")
   151  		},
   152  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   153  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   154  		},
   155  		Close: func(bd *stub.BalancerData) {
   156  			bd.Data.(balancer.Balancer).Close()
   157  		},
   158  	}
   159  
   160  	name := strings.ReplaceAll(strings.ToLower(t.Name()), "/", "")
   161  	stub.Register(name, bf)
   162  	json := fmt.Sprintf(`{"loadBalancingConfig": [{%q: {}}]}`, name)
   163  
   164  	opts := []grpc.DialOption{
   165  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   166  		grpc.WithDefaultServiceConfig(json),
   167  	}
   168  	cc, err := grpc.NewClient(backend.Address, opts...)
   169  	if err != nil {
   170  		t.Fatalf("grpc.NewClient(_) failed: %v", err)
   171  	}
   172  	defer cc.Close()
   173  
   174  	// The channel should remain in IDLE as the ExitIdle calls are not
   175  	// propagated to the lazy balancer from the stub balancer.
   176  	cc.Connect()
   177  	shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   178  	defer shortCancel()
   179  	testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Idle)
   180  
   181  	// The picker from the lazy balancer should be send to the channel when the
   182  	// first resolver update is received by lazy. Making an RPC should trigger
   183  	// child creation.
   184  	client := testgrpc.NewTestServiceClient(cc)
   185  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   186  		t.Errorf("client.EmptyCall() returned unexpected error: %v", err)
   187  	}
   188  }
   189  
   190  // Tests the scenario when a resolver produces a good state followed by a
   191  // resolver error. The test verifies that the child balancer receives the good
   192  // update followed by the error.
   193  func (s) TestGoodUpdateThenResolverError(t *testing.T) {
   194  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   195  	defer cancel()
   196  
   197  	backend := stubserver.StartTestService(t, nil)
   198  	defer backend.Stop()
   199  	resolverStateReceived := false
   200  	resolverErrorReceived := grpcsync.NewEvent()
   201  
   202  	childBF := stub.BalancerFuncs{
   203  		Init: func(bd *stub.BalancerData) {
   204  			bd.Data = balancer.Get(pickfirstleaf.Name).Build(bd.ClientConn, bd.BuildOptions)
   205  		},
   206  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   207  			if resolverErrorReceived.HasFired() {
   208  				t.Error("Received resolver error before resolver state.")
   209  			}
   210  			resolverStateReceived = true
   211  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   212  		},
   213  		ResolverError: func(bd *stub.BalancerData, err error) {
   214  			if !resolverStateReceived {
   215  				t.Error("Received resolver error before resolver state.")
   216  			}
   217  			resolverErrorReceived.Fire()
   218  			bd.Data.(balancer.Balancer).ResolverError(err)
   219  		},
   220  		Close: func(bd *stub.BalancerData) {
   221  			bd.Data.(balancer.Balancer).Close()
   222  		},
   223  	}
   224  
   225  	childBalName := strings.ReplaceAll(strings.ToLower(t.Name())+"_child", "/", "")
   226  	stub.Register(childBalName, childBF)
   227  
   228  	topLevelBF := stub.BalancerFuncs{
   229  		Init: func(bd *stub.BalancerData) {
   230  			bd.Data = lazy.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(childBalName).Build)
   231  		},
   232  		ExitIdle: func(bd *stub.BalancerData) {
   233  			t.Log("Ignoring call to ExitIdle to delay lazy child creation until RPC time.")
   234  		},
   235  		ResolverError: func(bd *stub.BalancerData, err error) {
   236  			bd.Data.(balancer.Balancer).ResolverError(err)
   237  		},
   238  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   239  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   240  		},
   241  		Close: func(bd *stub.BalancerData) {
   242  			bd.Data.(balancer.Balancer).Close()
   243  		},
   244  	}
   245  
   246  	topLevelBalName := strings.ReplaceAll(strings.ToLower(t.Name())+"_top_level", "/", "")
   247  	stub.Register(topLevelBalName, topLevelBF)
   248  
   249  	json := fmt.Sprintf(`{"loadBalancingConfig": [{%q: {}}]}`, topLevelBalName)
   250  
   251  	mr := manual.NewBuilderWithScheme("e2e-test")
   252  	defer mr.Close()
   253  
   254  	mr.InitialState(resolver.State{
   255  		Endpoints: []resolver.Endpoint{
   256  			{Addresses: []resolver.Address{{Addr: backend.Address}}},
   257  		},
   258  	})
   259  
   260  	opts := []grpc.DialOption{
   261  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   262  		grpc.WithResolvers(mr),
   263  		grpc.WithDefaultServiceConfig(json),
   264  	}
   265  	cc, err := grpc.NewClient(mr.Scheme()+":///whatever", opts...)
   266  	if err != nil {
   267  		t.Fatalf("grpc.NewClient(_) failed: %v", err)
   268  
   269  	}
   270  
   271  	defer cc.Close()
   272  	cc.Connect()
   273  
   274  	mr.CC().ReportError(errors.New("test error"))
   275  	// The channel should remain in IDLE as the ExitIdle calls are not
   276  	// propagated to the lazy balancer from the stub balancer.
   277  	shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   278  	defer shortCancel()
   279  	testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Idle)
   280  
   281  	client := testgrpc.NewTestServiceClient(cc)
   282  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   283  		t.Errorf("client.EmptyCall() returned unexpected error: %v", err)
   284  	}
   285  
   286  	if !resolverStateReceived {
   287  		t.Fatalf("Child balancer did not receive resolver state.")
   288  	}
   289  
   290  	select {
   291  	case <-resolverErrorReceived.Done():
   292  	case <-ctx.Done():
   293  		t.Fatal("Context timed out waiting for resolver error to be delivered to child balancer.")
   294  	}
   295  }
   296  
   297  // Tests the scenario when a resolver produces a list of endpoints followed by
   298  // a resolver error. The test verifies that the child balancer receives only the
   299  // good update.
   300  func (s) TestResolverErrorThenGoodUpdate(t *testing.T) {
   301  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   302  	defer cancel()
   303  
   304  	backend := stubserver.StartTestService(t, nil)
   305  	defer backend.Stop()
   306  
   307  	childBF := stub.BalancerFuncs{
   308  		Init: func(bd *stub.BalancerData) {
   309  			bd.Data = balancer.Get(pickfirstleaf.Name).Build(bd.ClientConn, bd.BuildOptions)
   310  		},
   311  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   312  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   313  		},
   314  		ResolverError: func(bd *stub.BalancerData, err error) {
   315  			t.Error("Received unexpected resolver error.")
   316  			bd.Data.(balancer.Balancer).ResolverError(err)
   317  		},
   318  		Close: func(bd *stub.BalancerData) {
   319  			bd.Data.(balancer.Balancer).Close()
   320  		},
   321  	}
   322  
   323  	childBalName := strings.ReplaceAll(strings.ToLower(t.Name())+"_child", "/", "")
   324  	stub.Register(childBalName, childBF)
   325  
   326  	topLevelBF := stub.BalancerFuncs{
   327  		Init: func(bd *stub.BalancerData) {
   328  			bd.Data = lazy.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(childBalName).Build)
   329  		},
   330  		ExitIdle: func(bd *stub.BalancerData) {
   331  			t.Log("Ignoring call to ExitIdle to delay lazy child creation until RPC time.")
   332  		},
   333  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   334  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   335  		},
   336  		Close: func(bd *stub.BalancerData) {
   337  			bd.Data.(balancer.Balancer).Close()
   338  		},
   339  	}
   340  
   341  	topLevelBalName := strings.ReplaceAll(strings.ToLower(t.Name())+"_top_level", "/", "")
   342  	stub.Register(topLevelBalName, topLevelBF)
   343  
   344  	json := fmt.Sprintf(`{"loadBalancingConfig": [{%q: {}}]}`, topLevelBalName)
   345  
   346  	mr := manual.NewBuilderWithScheme("e2e-test")
   347  	defer mr.Close()
   348  
   349  	mr.InitialState(resolver.State{
   350  		Endpoints: []resolver.Endpoint{
   351  			{Addresses: []resolver.Address{{Addr: backend.Address}}},
   352  		},
   353  	})
   354  
   355  	opts := []grpc.DialOption{
   356  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   357  		grpc.WithResolvers(mr),
   358  		grpc.WithDefaultServiceConfig(json),
   359  	}
   360  	cc, err := grpc.NewClient(mr.Scheme()+":///whatever", opts...)
   361  	if err != nil {
   362  		t.Fatalf("grpc.NewClient(_) failed: %v", err)
   363  
   364  	}
   365  
   366  	defer cc.Close()
   367  	cc.Connect()
   368  
   369  	// Send an error followed by a good update.
   370  	mr.CC().ReportError(errors.New("test error"))
   371  	mr.UpdateState(resolver.State{
   372  		Endpoints: []resolver.Endpoint{
   373  			{Addresses: []resolver.Address{{Addr: backend.Address}}},
   374  		},
   375  	})
   376  
   377  	// The channel should remain in IDLE as the ExitIdle calls are not
   378  	// propagated to the lazy balancer from the stub balancer.
   379  	shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   380  	defer shortCancel()
   381  	testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Idle)
   382  
   383  	// An RPC would succeed only if the leaf pickfirst receives the endpoint
   384  	// list.
   385  	client := testgrpc.NewTestServiceClient(cc)
   386  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   387  		t.Errorf("client.EmptyCall() returned unexpected error: %v", err)
   388  	}
   389  }
   390  
   391  // Tests that ExitIdle calls are correctly passed through to the child balancer.
   392  // It starts a backend and ensures the channel connects to it. The test then
   393  // stops the backend, making the channel enter IDLE. The test calls Connect on
   394  // the channel and verifies that the child balancer exits idle.
   395  func (s) TestExitIdlePassthrough(t *testing.T) {
   396  	backend1 := stubserver.StartTestService(t, nil)
   397  	defer backend1.Stop()
   398  
   399  	mr := manual.NewBuilderWithScheme("e2e-test")
   400  	defer mr.Close()
   401  
   402  	mr.InitialState(resolver.State{
   403  		Endpoints: []resolver.Endpoint{
   404  			{Addresses: []resolver.Address{{Addr: backend1.Address}}},
   405  		},
   406  	})
   407  
   408  	bf := stub.BalancerFuncs{
   409  		Init: func(bd *stub.BalancerData) {
   410  			bd.Data = lazy.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirstleaf.Name).Build)
   411  		},
   412  		ExitIdle: func(bd *stub.BalancerData) {
   413  			bd.Data.(balancer.ExitIdler).ExitIdle()
   414  		},
   415  		ResolverError: func(bd *stub.BalancerData, err error) {
   416  			bd.Data.(balancer.Balancer).ResolverError(err)
   417  		},
   418  		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
   419  			return bd.Data.(balancer.Balancer).UpdateClientConnState(ccs)
   420  		},
   421  		Close: func(bd *stub.BalancerData) {
   422  			bd.Data.(balancer.Balancer).Close()
   423  		},
   424  	}
   425  	stub.Register(t.Name(), bf)
   426  	json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s": {}}]}`, t.Name())
   427  	opts := []grpc.DialOption{
   428  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   429  		grpc.WithDefaultServiceConfig(json),
   430  		grpc.WithResolvers(mr),
   431  	}
   432  	cc, err := grpc.NewClient(mr.Scheme()+":///", opts...)
   433  	if err != nil {
   434  		t.Fatalf("grpc.NewClient(_) failed: %v", err)
   435  
   436  	}
   437  	defer cc.Close()
   438  
   439  	cc.Connect()
   440  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   441  	defer cancel()
   442  	testutils.AwaitState(ctx, t, cc, connectivity.Ready)
   443  
   444  	// Stopping the active backend should put the channel in IDLE.
   445  	backend1.Stop()
   446  	testutils.AwaitState(ctx, t, cc, connectivity.Idle)
   447  
   448  	// Sending a new backend address should not kick the channel out of IDLE.
   449  	// On calling cc.Connect(), the channel should call ExitIdle on the lazy
   450  	// balancer which passes through the call to the leaf pickfirst.
   451  	backend2 := stubserver.StartTestService(t, nil)
   452  	defer backend2.Stop()
   453  
   454  	mr.UpdateState(resolver.State{
   455  		Endpoints: []resolver.Endpoint{
   456  			{Addresses: []resolver.Address{{Addr: backend2.Address}}},
   457  		},
   458  	})
   459  
   460  	shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   461  	defer shortCancel()
   462  	testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Idle)
   463  
   464  	cc.Connect()
   465  	testutils.AwaitState(ctx, t, cc, connectivity.Ready)
   466  }