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