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 }