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 }