google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/xdslbregistry/xdslbregistry_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 xdslbregistry_test contains test cases for the xDS LB Policy Registry. 20 package xdslbregistry_test 21 22 import ( 23 "encoding/json" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 _ "google.golang.org/grpc/balancer/roundrobin" 29 "google.golang.org/grpc/internal/balancer/stub" 30 "google.golang.org/grpc/internal/envconfig" 31 "google.golang.org/grpc/internal/grpctest" 32 "google.golang.org/grpc/internal/pretty" 33 internalserviceconfig "google.golang.org/grpc/internal/serviceconfig" 34 "google.golang.org/grpc/internal/testutils" 35 _ "google.golang.org/grpc/xds" // Register the xDS LB Registry Converters. 36 "google.golang.org/grpc/xds/internal/balancer/wrrlocality" 37 "google.golang.org/grpc/xds/internal/xdsclient/xdslbregistry" 38 "google.golang.org/protobuf/proto" 39 "google.golang.org/protobuf/types/known/anypb" 40 "google.golang.org/protobuf/types/known/structpb" 41 "google.golang.org/protobuf/types/known/wrapperspb" 42 43 v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1" 44 v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3" 45 v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 46 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 47 v3leastrequestpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3" 48 v3pickfirstpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/pick_first/v3" 49 v3ringhashpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/ring_hash/v3" 50 v3roundrobinpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3" 51 v3wrrlocalitypb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3" 52 ) 53 54 type s struct { 55 grpctest.Tester 56 } 57 58 func Test(t *testing.T) { 59 grpctest.RunSubTests(t, s{}) 60 } 61 62 func wrrLocalityBalancerConfig(childPolicy *internalserviceconfig.BalancerConfig) *internalserviceconfig.BalancerConfig { 63 return &internalserviceconfig.BalancerConfig{ 64 Name: wrrlocality.Name, 65 Config: &wrrlocality.LBConfig{ 66 ChildPolicy: childPolicy, 67 }, 68 } 69 } 70 71 func (s) TestConvertToServiceConfigSuccess(t *testing.T) { 72 defer func(old bool) { envconfig.LeastRequestLB = old }(envconfig.LeastRequestLB) 73 envconfig.LeastRequestLB = false 74 75 const customLBPolicyName = "myorg.MyCustomLeastRequestPolicy" 76 stub.Register(customLBPolicyName, stub.BalancerFuncs{}) 77 78 tests := []struct { 79 name string 80 policy *v3clusterpb.LoadBalancingPolicy 81 wantConfig string // JSON config 82 lrEnabled bool 83 }{ 84 { 85 name: "ring_hash", 86 policy: &v3clusterpb.LoadBalancingPolicy{ 87 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 88 { 89 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 90 TypedConfig: testutils.MarshalAny(t, &v3ringhashpb.RingHash{ 91 HashFunction: v3ringhashpb.RingHash_XX_HASH, 92 MinimumRingSize: wrapperspb.UInt64(10), 93 MaximumRingSize: wrapperspb.UInt64(100), 94 }), 95 }, 96 }, 97 }, 98 }, 99 wantConfig: `[{"ring_hash_experimental": { "minRingSize": 10, "maxRingSize": 100 }}]`, 100 }, 101 { 102 name: "least_request", 103 policy: &v3clusterpb.LoadBalancingPolicy{ 104 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 105 { 106 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 107 TypedConfig: testutils.MarshalAny(t, &v3leastrequestpb.LeastRequest{ 108 ChoiceCount: wrapperspb.UInt32(3), 109 }), 110 }, 111 }, 112 }, 113 }, 114 wantConfig: `[{"least_request_experimental": { "choiceCount": 3 }}]`, 115 lrEnabled: true, 116 }, 117 { 118 name: "pick_first_shuffle", 119 policy: &v3clusterpb.LoadBalancingPolicy{ 120 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 121 { 122 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 123 TypedConfig: testutils.MarshalAny(t, &v3pickfirstpb.PickFirst{ 124 ShuffleAddressList: true, 125 }), 126 }, 127 }, 128 }, 129 }, 130 wantConfig: `[{"pick_first": { "shuffleAddressList": true }}]`, 131 }, 132 { 133 name: "pick_first", 134 policy: &v3clusterpb.LoadBalancingPolicy{ 135 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 136 { 137 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 138 TypedConfig: testutils.MarshalAny(t, &v3pickfirstpb.PickFirst{}), 139 }, 140 }, 141 }, 142 }, 143 wantConfig: `[{"pick_first": { "shuffleAddressList": false }}]`, 144 }, 145 { 146 name: "round_robin", 147 policy: &v3clusterpb.LoadBalancingPolicy{ 148 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 149 { 150 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 151 TypedConfig: testutils.MarshalAny(t, &v3roundrobinpb.RoundRobin{}), 152 }, 153 }, 154 }, 155 }, 156 wantConfig: `[{"round_robin": {}}]`, 157 }, 158 { 159 name: "round_robin_ring_hash_use_first_supported", 160 policy: &v3clusterpb.LoadBalancingPolicy{ 161 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 162 { 163 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 164 TypedConfig: testutils.MarshalAny(t, &v3roundrobinpb.RoundRobin{}), 165 }, 166 }, 167 { 168 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 169 TypedConfig: testutils.MarshalAny(t, &v3ringhashpb.RingHash{ 170 HashFunction: v3ringhashpb.RingHash_XX_HASH, 171 MinimumRingSize: wrapperspb.UInt64(10), 172 MaximumRingSize: wrapperspb.UInt64(100), 173 }), 174 }, 175 }, 176 }, 177 }, 178 wantConfig: `[{"round_robin": {}}]`, 179 }, 180 { 181 name: "pf_rr_use_pick_first", 182 policy: &v3clusterpb.LoadBalancingPolicy{ 183 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 184 { 185 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 186 TypedConfig: testutils.MarshalAny(t, &v3pickfirstpb.PickFirst{ 187 ShuffleAddressList: true, 188 }), 189 }, 190 }, 191 { 192 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 193 TypedConfig: testutils.MarshalAny(t, &v3roundrobinpb.RoundRobin{}), 194 }, 195 }, 196 }, 197 }, 198 wantConfig: `[{"pick_first": { "shuffleAddressList": true }}]`, 199 }, 200 { 201 name: "least_request_disabled_pf_rr_use_first_supported", 202 policy: &v3clusterpb.LoadBalancingPolicy{ 203 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 204 { 205 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 206 TypedConfig: testutils.MarshalAny(t, &v3leastrequestpb.LeastRequest{ 207 ChoiceCount: wrapperspb.UInt32(32), 208 }), 209 }, 210 }, 211 { 212 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 213 TypedConfig: testutils.MarshalAny(t, &v3roundrobinpb.RoundRobin{}), 214 }, 215 }, 216 }, 217 }, 218 wantConfig: `[{"round_robin": {}}]`, 219 }, 220 { 221 name: "custom_lb_type_v3_struct", 222 policy: &v3clusterpb.LoadBalancingPolicy{ 223 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 224 { 225 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 226 // The type not registered in gRPC Policy registry. 227 // Should fallback to next policy in list. 228 TypedConfig: testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 229 TypeUrl: "type.googleapis.com/myorg.ThisTypeDoesNotExist", 230 Value: &structpb.Struct{}, 231 }), 232 }, 233 }, 234 { 235 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 236 TypedConfig: testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 237 TypeUrl: "type.googleapis.com/myorg.MyCustomLeastRequestPolicy", 238 Value: &structpb.Struct{}, 239 }), 240 }, 241 }, 242 }, 243 }, 244 wantConfig: `[{"myorg.MyCustomLeastRequestPolicy": {}}]`, 245 }, 246 { 247 name: "custom_lb_type_v1_struct", 248 policy: &v3clusterpb.LoadBalancingPolicy{ 249 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 250 { 251 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 252 TypedConfig: testutils.MarshalAny(t, &v1xdsudpatypepb.TypedStruct{ 253 TypeUrl: "type.googleapis.com/myorg.MyCustomLeastRequestPolicy", 254 Value: &structpb.Struct{}, 255 }), 256 }, 257 }, 258 }, 259 }, 260 wantConfig: `[{"myorg.MyCustomLeastRequestPolicy": {}}]`, 261 }, 262 { 263 name: "wrr_locality_child_round_robin", 264 policy: &v3clusterpb.LoadBalancingPolicy{ 265 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 266 { 267 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 268 TypedConfig: wrrLocalityAny(t, &v3roundrobinpb.RoundRobin{}), 269 }, 270 }, 271 }, 272 }, 273 wantConfig: `[{"xds_wrr_locality_experimental": { "childPolicy": [{"round_robin": {}}] }}]`, 274 }, 275 { 276 name: "wrr_locality_child_custom_lb_type_v3_struct", 277 policy: &v3clusterpb.LoadBalancingPolicy{ 278 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 279 { 280 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 281 TypedConfig: wrrLocalityAny(t, &v3xdsxdstypepb.TypedStruct{ 282 TypeUrl: "type.googleapis.com/myorg.MyCustomLeastRequestPolicy", 283 Value: &structpb.Struct{}, 284 }), 285 }, 286 }, 287 }, 288 }, 289 wantConfig: `[{"xds_wrr_locality_experimental": { "childPolicy": [{"myorg.MyCustomLeastRequestPolicy": {}}] }}]`, 290 }, 291 { 292 name: "on-the-boundary-of-recursive-limit", 293 policy: &v3clusterpb.LoadBalancingPolicy{ 294 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 295 { 296 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 297 TypedConfig: wrrLocalityAny(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, &v3roundrobinpb.RoundRobin{}))))))))))))))), 298 }, 299 }, 300 }, 301 }, 302 wantConfig: jsonMarshal(t, wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(wrrLocalityBalancerConfig(&internalserviceconfig.BalancerConfig{ 303 Name: "round_robin", 304 })))))))))))))))), 305 }, 306 } 307 308 for _, test := range tests { 309 t.Run(test.name, func(t *testing.T) { 310 if test.lrEnabled { 311 defer func(old bool) { envconfig.LeastRequestLB = old }(envconfig.LeastRequestLB) 312 envconfig.LeastRequestLB = true 313 } 314 rawJSON, err := xdslbregistry.ConvertToServiceConfig(test.policy, 0) 315 if err != nil { 316 t.Fatalf("ConvertToServiceConfig(%s) failed: %v", pretty.ToJSON(test.policy), err) 317 } 318 // got and want must be unmarshalled since JSON strings shouldn't 319 // generally be directly compared. 320 var got []map[string]any 321 if err := json.Unmarshal(rawJSON, &got); err != nil { 322 t.Fatalf("Error unmarshalling rawJSON (%q): %v", rawJSON, err) 323 } 324 var want []map[string]any 325 if err := json.Unmarshal(json.RawMessage(test.wantConfig), &want); err != nil { 326 t.Fatalf("Error unmarshalling wantConfig (%q): %v", test.wantConfig, err) 327 } 328 if diff := cmp.Diff(got, want); diff != "" { 329 t.Fatalf("ConvertToServiceConfig() got unexpected output, diff (-got +want): %v", diff) 330 } 331 }) 332 } 333 } 334 335 func jsonMarshal(t *testing.T, x any) string { 336 t.Helper() 337 js, err := json.Marshal(x) 338 if err != nil { 339 t.Fatalf("Error marshalling to JSON (%+v): %v", x, err) 340 } 341 return string(js) 342 } 343 344 // TestConvertToServiceConfigFailure tests failure cases of the xDS LB registry 345 // of converting proto configuration to JSON configuration. 346 func (s) TestConvertToServiceConfigFailure(t *testing.T) { 347 tests := []struct { 348 name string 349 policy *v3clusterpb.LoadBalancingPolicy 350 wantErr string 351 }{ 352 { 353 name: "not xx_hash function", 354 policy: &v3clusterpb.LoadBalancingPolicy{ 355 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 356 { 357 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 358 TypedConfig: testutils.MarshalAny(t, &v3ringhashpb.RingHash{ 359 HashFunction: v3ringhashpb.RingHash_MURMUR_HASH_2, 360 MinimumRingSize: wrapperspb.UInt64(10), 361 MaximumRingSize: wrapperspb.UInt64(100), 362 }), 363 }, 364 }, 365 }, 366 }, 367 wantErr: "unsupported ring_hash hash function", 368 }, 369 { 370 name: "no-supported-policy", 371 policy: &v3clusterpb.LoadBalancingPolicy{ 372 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 373 { 374 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 375 // The type not registered in gRPC Policy registry. 376 TypedConfig: testutils.MarshalAny(t, &v3xdsxdstypepb.TypedStruct{ 377 TypeUrl: "type.googleapis.com/myorg.ThisTypeDoesNotExist", 378 Value: &structpb.Struct{}, 379 }), 380 }, 381 }, 382 { 383 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 384 // Not supported by gRPC-Go. 385 TypedConfig: testutils.MarshalAny(t, &v3leastrequestpb.LeastRequest{}), 386 }, 387 }, 388 }, 389 }, 390 wantErr: "no supported policy found in policy list", 391 }, 392 { 393 name: "exceeds-boundary-of-recursive-limit-by-1", 394 policy: &v3clusterpb.LoadBalancingPolicy{ 395 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 396 { 397 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 398 TypedConfig: wrrLocalityAny(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, wrrLocality(t, &v3roundrobinpb.RoundRobin{})))))))))))))))), 399 }, 400 }, 401 }, 402 }, 403 wantErr: "exceeds max depth", 404 }, 405 } 406 407 for _, test := range tests { 408 t.Run(test.name, func(t *testing.T) { 409 _, gotErr := xdslbregistry.ConvertToServiceConfig(test.policy, 0) 410 // Test the error substring to test the different root causes of 411 // errors. This is more brittle over time, but it's important to 412 // test the root cause of the errors emitted from the 413 // ConvertToServiceConfig function call. Also, this package owns the 414 // error strings so breakages won't come unexpectedly. 415 if gotErr == nil || !strings.Contains(gotErr.Error(), test.wantErr) { 416 t.Fatalf("ConvertToServiceConfig() = %v, wantErr %v", gotErr, test.wantErr) 417 } 418 }) 419 } 420 } 421 422 // wrrLocality is a helper that takes a proto message and returns a 423 // WrrLocalityProto with the proto message marshaled into a proto.Any as a 424 // child. 425 func wrrLocality(t *testing.T, m proto.Message) *v3wrrlocalitypb.WrrLocality { 426 return &v3wrrlocalitypb.WrrLocality{ 427 EndpointPickingPolicy: &v3clusterpb.LoadBalancingPolicy{ 428 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 429 { 430 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 431 TypedConfig: testutils.MarshalAny(t, m), 432 }, 433 }, 434 }, 435 }, 436 } 437 } 438 439 // wrrLocalityAny takes a proto message and returns a wrr locality proto 440 // marshaled as an any with an any child set to the marshaled proto message. 441 func wrrLocalityAny(t *testing.T, m proto.Message) *anypb.Any { 442 return testutils.MarshalAny(t, wrrLocality(t, m)) 443 }