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 }