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  }