Skip to content

Commit 5ce5686

Browse files
authored
pickfirst: guard config parsing on GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG (#6470)
1 parent 41d1232 commit 5ce5686

File tree

2 files changed

+125
-11
lines changed

2 files changed

+125
-11
lines changed

pickfirst.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,21 @@ type pfConfig struct {
5858
}
5959

6060
func (*pickfirstBuilder) ParseConfig(js json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
61-
cfg := &pfConfig{}
62-
if err := json.Unmarshal(js, cfg); err != nil {
61+
if !envconfig.PickFirstLBConfig {
62+
// Prior to supporting loadbalancing configuration, the pick_first LB
63+
// policy did not implement the balancer.ConfigParser interface. This
64+
// meant that if a non-empty configuration was passed to it, the service
65+
// config unmarshaling code would throw a warning log, but would
66+
// continue using the pick_first LB policy. The code below ensures the
67+
// same behavior is retained if the env var is not set.
68+
if string(js) != "{}" {
69+
logger.Warningf("Ignoring non-empty balancer configuration %q for the pick_first LB policy", string(js))
70+
}
71+
return nil, nil
72+
}
73+
74+
var cfg pfConfig
75+
if err := json.Unmarshal(js, &cfg); err != nil {
6376
return nil, fmt.Errorf("pickfirst: unable to unmarshal LB policy config: %s, error: %v", string(js), err)
6477
}
6578
return cfg, nil
@@ -69,7 +82,6 @@ type pickfirstBalancer struct {
6982
state connectivity.State
7083
cc balancer.ClientConn
7184
subConn balancer.SubConn
72-
cfg *pfConfig
7385
}
7486

7587
func (b *pickfirstBalancer) ResolverError(err error) {
@@ -106,18 +118,17 @@ func (b *pickfirstBalancer) UpdateClientConnState(state balancer.ClientConnState
106118
return balancer.ErrBadResolverState
107119
}
108120

109-
if state.BalancerConfig != nil {
110-
cfg, ok := state.BalancerConfig.(*pfConfig)
111-
if !ok {
112-
return fmt.Errorf("pickfirstBalancer: received nil or illegal BalancerConfig (type %T): %v", state.BalancerConfig, state.BalancerConfig)
113-
}
114-
b.cfg = cfg
121+
// We don't have to guard this block with the env var because ParseConfig
122+
// already does so.
123+
cfg, ok := state.BalancerConfig.(pfConfig)
124+
if state.BalancerConfig != nil && !ok {
125+
return fmt.Errorf("pickfirstBalancer: received illegal BalancerConfig (type %T): %v", state.BalancerConfig, state.BalancerConfig)
115126
}
116-
117-
if envconfig.PickFirstLBConfig && b.cfg != nil && b.cfg.ShuffleAddressList {
127+
if cfg.ShuffleAddressList {
118128
addrs = append([]resolver.Address{}, addrs...)
119129
grpcrand.Shuffle(len(addrs), func(i, j int) { addrs[i], addrs[j] = addrs[j], addrs[i] })
120130
}
131+
121132
if b.subConn != nil {
122133
b.cc.UpdateAddresses(b.subConn, addrs)
123134
return nil

test/pickfirst_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"google.golang.org/grpc/codes"
3030
"google.golang.org/grpc/connectivity"
3131
"google.golang.org/grpc/credentials/insecure"
32+
"google.golang.org/grpc/internal"
3233
"google.golang.org/grpc/internal/channelz"
3334
"google.golang.org/grpc/internal/envconfig"
3435
"google.golang.org/grpc/internal/grpcrand"
@@ -37,6 +38,7 @@ import (
3738
"google.golang.org/grpc/internal/testutils/pickfirst"
3839
"google.golang.org/grpc/resolver"
3940
"google.golang.org/grpc/resolver/manual"
41+
"google.golang.org/grpc/serviceconfig"
4042
"google.golang.org/grpc/status"
4143

4244
testgrpc "google.golang.org/grpc/interop/grpc_testing"
@@ -468,6 +470,107 @@ func (s) TestPickFirst_ShuffleAddressListDisabled(t *testing.T) {
468470
}
469471
}
470472

473+
// Test config parsing with the env var turned on and off for various scenarios.
474+
func (s) TestPickFirst_ParseConfig_Success(t *testing.T) {
475+
// Install a shuffler that always reverses two entries.
476+
origShuf := grpcrand.Shuffle
477+
defer func() { grpcrand.Shuffle = origShuf }()
478+
grpcrand.Shuffle = func(n int, f func(int, int)) {
479+
if n != 2 {
480+
t.Errorf("Shuffle called with n=%v; want 2", n)
481+
return
482+
}
483+
f(0, 1) // reverse the two addresses
484+
}
485+
486+
tests := []struct {
487+
name string
488+
envVar bool
489+
serviceConfig string
490+
wantFirstAddr bool
491+
}{
492+
{
493+
name: "env var disabled with empty pickfirst config",
494+
envVar: false,
495+
serviceConfig: `{"loadBalancingConfig": [{"pick_first":{}}]}`,
496+
wantFirstAddr: true,
497+
},
498+
{
499+
name: "env var disabled with non-empty good pickfirst config",
500+
envVar: false,
501+
serviceConfig: `{"loadBalancingConfig": [{"pick_first":{ "shuffleAddressList": true }}]}`,
502+
wantFirstAddr: true,
503+
},
504+
{
505+
name: "env var disabled with non-empty bad pickfirst config",
506+
envVar: false,
507+
serviceConfig: `{"loadBalancingConfig": [{"pick_first":{ "shuffleAddressList": 666 }}]}`,
508+
wantFirstAddr: true,
509+
},
510+
{
511+
name: "env var enabled with empty pickfirst config",
512+
envVar: true,
513+
serviceConfig: `{"loadBalancingConfig": [{"pick_first":{}}]}`,
514+
wantFirstAddr: true,
515+
},
516+
{
517+
name: "env var enabled with empty good pickfirst config",
518+
envVar: true,
519+
serviceConfig: `{"loadBalancingConfig": [{"pick_first":{ "shuffleAddressList": true }}]}`,
520+
wantFirstAddr: false,
521+
},
522+
}
523+
524+
for _, test := range tests {
525+
t.Run(test.name, func(t *testing.T) {
526+
// Set the env var as specified by the test table.
527+
origPickFirstLBConfig := envconfig.PickFirstLBConfig
528+
envconfig.PickFirstLBConfig = test.envVar
529+
defer func() { envconfig.PickFirstLBConfig = origPickFirstLBConfig }()
530+
531+
// Set up our backends.
532+
cc, r, backends := setupPickFirst(t, 2)
533+
addrs := stubBackendsToResolverAddrs(backends)
534+
535+
r.UpdateState(resolver.State{
536+
ServiceConfig: parseServiceConfig(t, r, test.serviceConfig),
537+
Addresses: addrs,
538+
})
539+
540+
// Some tests expect address shuffling to happen, and indicate that
541+
// by setting wantFirstAddr to false (since our shuffling function
542+
// defined at the top of this test, simply reverses the list of
543+
// addresses provided to it).
544+
wantAddr := addrs[0]
545+
if !test.wantFirstAddr {
546+
wantAddr = addrs[1]
547+
}
548+
549+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
550+
defer cancel()
551+
if err := pickfirst.CheckRPCsToBackend(ctx, cc, wantAddr); err != nil {
552+
t.Fatal(err)
553+
}
554+
})
555+
}
556+
}
557+
558+
// Test config parsing for a bad service config.
559+
func (s) TestPickFirst_ParseConfig_Failure(t *testing.T) {
560+
origPickFirstLBConfig := envconfig.PickFirstLBConfig
561+
envconfig.PickFirstLBConfig = true
562+
defer func() { envconfig.PickFirstLBConfig = origPickFirstLBConfig }()
563+
564+
// Service config should fail with the below config. Name resolvers are
565+
// expected to perform this parsing before they push the parsed service
566+
// config to the channel.
567+
const sc = `{"loadBalancingConfig": [{"pick_first":{ "shuffleAddressList": 666 }}]}`
568+
scpr := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(sc)
569+
if scpr.Err == nil {
570+
t.Fatalf("ParseConfig() succeeded and returned %+v, when expected to fail", scpr)
571+
}
572+
}
573+
471574
// setupPickFirstWithListenerWrapper is very similar to setupPickFirst, but uses
472575
// a wrapped listener that the test can use to track accepted connections.
473576
func setupPickFirstWithListenerWrapper(t *testing.T, backendCount int, opts ...grpc.DialOption) (*grpc.ClientConn, *manual.Resolver, []*stubserver.StubServer, []*testutils.ListenerWrapper) {

0 commit comments

Comments
 (0)