google.golang.org/grpc@v1.74.2/xds/internal/balancer/wrrlocality/balancer_test.go (about) 1 /* 2 * 3 * Copyright 2023 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 wrrlocality 20 21 import ( 22 "context" 23 "encoding/json" 24 "errors" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "google.golang.org/grpc/balancer" 31 "google.golang.org/grpc/balancer/roundrobin" 32 "google.golang.org/grpc/balancer/weightedtarget" 33 "google.golang.org/grpc/internal/balancer/stub" 34 "google.golang.org/grpc/internal/grpctest" 35 internalserviceconfig "google.golang.org/grpc/internal/serviceconfig" 36 "google.golang.org/grpc/internal/testutils" 37 "google.golang.org/grpc/resolver" 38 "google.golang.org/grpc/serviceconfig" 39 "google.golang.org/grpc/xds/internal" 40 "google.golang.org/grpc/xds/internal/clients" 41 ) 42 43 const ( 44 defaultTestTimeout = 5 * time.Second 45 ) 46 47 type s struct { 48 grpctest.Tester 49 } 50 51 func Test(t *testing.T) { 52 grpctest.RunSubTests(t, s{}) 53 } 54 55 func (s) TestParseConfig(t *testing.T) { 56 const errParseConfigName = "errParseConfigBalancer" 57 stub.Register(errParseConfigName, stub.BalancerFuncs{ 58 ParseConfig: func(json.RawMessage) (serviceconfig.LoadBalancingConfig, error) { 59 return nil, errors.New("some error") 60 }, 61 }) 62 63 parser := bb{} 64 tests := []struct { 65 name string 66 input string 67 wantCfg serviceconfig.LoadBalancingConfig 68 wantErr string 69 }{ 70 { 71 name: "happy-case-round robin-child", 72 input: `{"childPolicy": [{"round_robin": {}}]}`, 73 wantCfg: &LBConfig{ 74 ChildPolicy: &internalserviceconfig.BalancerConfig{ 75 Name: roundrobin.Name, 76 }, 77 }, 78 }, 79 { 80 name: "invalid-json", 81 input: "{{invalidjson{{", 82 wantErr: "invalid character", 83 }, 84 85 { 86 name: "child-policy-field-isn't-set", 87 input: `{}`, 88 wantErr: "child policy field must be set", 89 }, 90 { 91 name: "child-policy-type-is-empty", 92 input: `{"childPolicy": []}`, 93 wantErr: "invalid loadBalancingConfig: no supported policies found in []", 94 }, 95 { 96 name: "child-policy-empty-config", 97 input: `{"childPolicy": [{"": {}}]}`, 98 wantErr: "invalid loadBalancingConfig: no supported policies found in []", 99 }, 100 { 101 name: "child-policy-type-isn't-registered", 102 input: `{"childPolicy": [{"doesNotExistBalancer": {"cluster": "test_cluster"}}]}`, 103 wantErr: "invalid loadBalancingConfig: no supported policies found in [doesNotExistBalancer]", 104 }, 105 { 106 name: "child-policy-config-is-invalid", 107 input: `{"childPolicy": [{"errParseConfigBalancer": {"cluster": "test_cluster"}}]}`, 108 wantErr: "error parsing loadBalancingConfig for policy \"errParseConfigBalancer\"", 109 }, 110 } 111 for _, test := range tests { 112 t.Run(test.name, func(t *testing.T) { 113 gotCfg, gotErr := parser.ParseConfig(json.RawMessage(test.input)) 114 // Substring match makes this very tightly coupled to the 115 // internalserviceconfig.BalancerConfig error strings. However, it 116 // is important to distinguish the different types of error messages 117 // possible as the parser has a few defined buckets of ways it can 118 // error out. 119 if (gotErr != nil) != (test.wantErr != "") { 120 t.Fatalf("ParseConfig(%v) = %v, wantErr %v", test.input, gotErr, test.wantErr) 121 } 122 if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) { 123 t.Fatalf("ParseConfig(%v) = %v, wantErr %v", test.input, gotErr, test.wantErr) 124 } 125 if test.wantErr != "" { 126 return 127 } 128 if diff := cmp.Diff(gotCfg, test.wantCfg); diff != "" { 129 t.Fatalf("ParseConfig(%v) got unexpected output, diff (-got +want): %v", test.input, diff) 130 } 131 }) 132 } 133 } 134 135 // TestUpdateClientConnState tests the UpdateClientConnState method of the 136 // wrr_locality_experimental balancer. This UpdateClientConn operation should 137 // take the localities and their weights in the addresses passed in, alongside 138 // the endpoint picking policy defined in the Balancer Config and construct a 139 // weighted target configuration corresponding to these inputs. 140 func (s) TestUpdateClientConnState(t *testing.T) { 141 // Configure the stub balancer defined below as the child policy of 142 // wrrLocalityBalancer. 143 cfgCh := testutils.NewChannel() 144 oldWeightedTargetName := weightedTargetName 145 defer func() { 146 weightedTargetName = oldWeightedTargetName 147 }() 148 weightedTargetName = "fake_weighted_target" 149 stub.Register("fake_weighted_target", stub.BalancerFuncs{ 150 ParseConfig: func(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) { 151 var cfg weightedtarget.LBConfig 152 if err := json.Unmarshal(c, &cfg); err != nil { 153 return nil, err 154 } 155 return &cfg, nil 156 }, 157 UpdateClientConnState: func(_ *stub.BalancerData, ccs balancer.ClientConnState) error { 158 wtCfg, ok := ccs.BalancerConfig.(*weightedtarget.LBConfig) 159 if !ok { 160 return errors.New("child received config that was not a weighted target config") 161 } 162 defer cfgCh.Send(wtCfg) 163 return nil 164 }, 165 }) 166 167 builder := balancer.Get(Name) 168 if builder == nil { 169 t.Fatalf("balancer.Get(%q) returned nil", Name) 170 } 171 tcc := testutils.NewBalancerClientConn(t) 172 bal := builder.Build(tcc, balancer.BuildOptions{}) 173 defer bal.Close() 174 wrrL := bal.(*wrrLocalityBalancer) 175 176 // Create the addresses with two localities with certain locality weights. 177 // This represents what addresses the wrr_locality balancer will receive in 178 // UpdateClientConnState. 179 addr1 := resolver.Address{ 180 Addr: "locality-1", 181 } 182 addr1 = internal.SetLocalityID(addr1, clients.Locality{ 183 Region: "region-1", 184 Zone: "zone-1", 185 SubZone: "subzone-1", 186 }) 187 addr1 = SetAddrInfo(addr1, AddrInfo{LocalityWeight: 2}) 188 189 addr2 := resolver.Address{ 190 Addr: "locality-2", 191 } 192 addr2 = internal.SetLocalityID(addr2, clients.Locality{ 193 Region: "region-2", 194 Zone: "zone-2", 195 SubZone: "subzone-2", 196 }) 197 addr2 = SetAddrInfo(addr2, AddrInfo{LocalityWeight: 1}) 198 addrs := []resolver.Address{addr1, addr2} 199 200 err := wrrL.UpdateClientConnState(balancer.ClientConnState{ 201 BalancerConfig: &LBConfig{ 202 ChildPolicy: &internalserviceconfig.BalancerConfig{ 203 Name: "round_robin", 204 }, 205 }, 206 ResolverState: resolver.State{ 207 Addresses: addrs, 208 }, 209 }) 210 if err != nil { 211 t.Fatalf("Unexpected error from UpdateClientConnState: %v", err) 212 } 213 214 // Note that these inline strings declared as the key in Targets built from 215 // Locality ID are not exactly what is shown in the example in the gRFC. 216 // However, this is an implementation detail that does not affect 217 // correctness (confirmed with Java team). The important thing is to get 218 // those three pieces of information region, zone, and subzone down to the 219 // child layer. 220 wantWtCfg := &weightedtarget.LBConfig{ 221 Targets: map[string]weightedtarget.Target{ 222 "{region=\"region-1\", zone=\"zone-1\", sub_zone=\"subzone-1\"}": { 223 Weight: 2, 224 ChildPolicy: &internalserviceconfig.BalancerConfig{ 225 Name: "round_robin", 226 }, 227 }, 228 "{region=\"region-2\", zone=\"zone-2\", sub_zone=\"subzone-2\"}": { 229 Weight: 1, 230 ChildPolicy: &internalserviceconfig.BalancerConfig{ 231 Name: "round_robin", 232 }, 233 }, 234 }, 235 } 236 237 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 238 defer cancel() 239 cfg, err := cfgCh.Receive(ctx) 240 if err != nil { 241 t.Fatalf("No signal received from UpdateClientConnState() on the child: %v", err) 242 } 243 244 gotWtCfg, ok := cfg.(*weightedtarget.LBConfig) 245 if !ok { 246 // Shouldn't happen - only sends a config on this channel. 247 t.Fatalf("Unexpected config type: %T", gotWtCfg) 248 } 249 250 if diff := cmp.Diff(gotWtCfg, wantWtCfg); diff != "" { 251 t.Fatalf("Child received unexpected config, diff (-got, +want): %v", diff) 252 } 253 }