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  }