google.golang.org/grpc@v1.72.2/xds/internal/balancer/ringhash/picker_test.go (about)

     1  /*
     2   *
     3   * Copyright 2021 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 ringhash
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"math"
    26  	"testing"
    27  	"time"
    28  
    29  	"google.golang.org/grpc/balancer"
    30  	"google.golang.org/grpc/connectivity"
    31  	"google.golang.org/grpc/internal/testutils"
    32  	"google.golang.org/grpc/metadata"
    33  )
    34  
    35  var (
    36  	testSubConns []*testutils.TestSubConn
    37  	errPicker    = errors.New("picker in TransientFailure")
    38  )
    39  
    40  func init() {
    41  	for i := 0; i < 8; i++ {
    42  		testSubConns = append(testSubConns, testutils.NewTestSubConn(fmt.Sprint(i)))
    43  	}
    44  }
    45  
    46  // fakeChildPicker is used to mock pickers from child pickfirst balancers.
    47  type fakeChildPicker struct {
    48  	connectivityState connectivity.State
    49  	subConn           *testutils.TestSubConn
    50  	tfError           error
    51  }
    52  
    53  func (p *fakeChildPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
    54  	switch p.connectivityState {
    55  	case connectivity.Idle:
    56  		p.subConn.Connect()
    57  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
    58  	case connectivity.Connecting:
    59  		return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
    60  	case connectivity.Ready:
    61  		return balancer.PickResult{SubConn: p.subConn}, nil
    62  	default:
    63  		return balancer.PickResult{}, p.tfError
    64  	}
    65  }
    66  
    67  type fakeExitIdler struct {
    68  	sc *testutils.TestSubConn
    69  }
    70  
    71  func (ei *fakeExitIdler) ExitIdle() {
    72  	ei.sc.Connect()
    73  }
    74  
    75  func testRingAndEndpointStates(states []connectivity.State) (*ring, map[string]endpointState) {
    76  	var items []*ringEntry
    77  	epStates := map[string]endpointState{}
    78  	for i, st := range states {
    79  		testSC := testSubConns[i]
    80  		items = append(items, &ringEntry{
    81  			idx:     i,
    82  			hash:    math.MaxUint64 / uint64(len(states)) * uint64(i),
    83  			hashKey: testSC.String(),
    84  		})
    85  		epState := endpointState{
    86  			state: balancer.State{
    87  				ConnectivityState: st,
    88  				Picker: &fakeChildPicker{
    89  					connectivityState: st,
    90  					tfError:           fmt.Errorf("%d: %w", i, errPicker),
    91  					subConn:           testSC,
    92  				},
    93  			},
    94  			balancer: &fakeExitIdler{
    95  				sc: testSC,
    96  			},
    97  		}
    98  		epStates[testSC.String()] = epState
    99  	}
   100  	return &ring{items: items}, epStates
   101  }
   102  
   103  func (s) TestPickerPickFirstTwo(t *testing.T) {
   104  	tests := []struct {
   105  		name               string
   106  		connectivityStates []connectivity.State
   107  		wantSC             balancer.SubConn
   108  		wantErr            error
   109  		wantSCToConnect    balancer.SubConn
   110  	}{
   111  		{
   112  			name:               "picked is Ready",
   113  			connectivityStates: []connectivity.State{connectivity.Ready, connectivity.Idle},
   114  			wantSC:             testSubConns[0],
   115  		},
   116  		{
   117  			name:               "picked is connecting, queue",
   118  			connectivityStates: []connectivity.State{connectivity.Connecting, connectivity.Idle},
   119  			wantErr:            balancer.ErrNoSubConnAvailable,
   120  		},
   121  		{
   122  			name:               "picked is Idle, connect and queue",
   123  			connectivityStates: []connectivity.State{connectivity.Idle, connectivity.Idle},
   124  			wantErr:            balancer.ErrNoSubConnAvailable,
   125  			wantSCToConnect:    testSubConns[0],
   126  		},
   127  		{
   128  			name:               "picked is TransientFailure, next is ready, return",
   129  			connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Ready},
   130  			wantSC:             testSubConns[1],
   131  		},
   132  		{
   133  			name:               "picked is TransientFailure, next is connecting, queue",
   134  			connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Connecting},
   135  			wantErr:            balancer.ErrNoSubConnAvailable,
   136  		},
   137  		{
   138  			name:               "picked is TransientFailure, next is Idle, connect and queue",
   139  			connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Idle},
   140  			wantErr:            balancer.ErrNoSubConnAvailable,
   141  			wantSCToConnect:    testSubConns[1],
   142  		},
   143  		{
   144  			name:               "all are in TransientFailure, return picked failure",
   145  			connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.TransientFailure},
   146  			wantErr:            errPicker,
   147  		},
   148  	}
   149  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   150  	defer cancel()
   151  	for _, tt := range tests {
   152  		t.Run(tt.name, func(t *testing.T) {
   153  			ring, epStates := testRingAndEndpointStates(tt.connectivityStates)
   154  			p := &picker{
   155  				ring:           ring,
   156  				endpointStates: epStates,
   157  			}
   158  			got, err := p.Pick(balancer.PickInfo{
   159  				Ctx: SetXDSRequestHash(ctx, 0), // always pick the first endpoint on the ring.
   160  			})
   161  			if (err != nil || tt.wantErr != nil) && !errors.Is(err, tt.wantErr) {
   162  				t.Errorf("Pick() error = %v, wantErr %v", err, tt.wantErr)
   163  				return
   164  			}
   165  			if got.SubConn != tt.wantSC {
   166  				t.Errorf("Pick() got = %v, want picked SubConn: %v", got, tt.wantSC)
   167  			}
   168  			if sc := tt.wantSCToConnect; sc != nil {
   169  				select {
   170  				case <-sc.(*testutils.TestSubConn).ConnectCh:
   171  				case <-time.After(defaultTestShortTimeout):
   172  					t.Errorf("timeout waiting for Connect() from SubConn %v", sc)
   173  				}
   174  			}
   175  		})
   176  	}
   177  }
   178  
   179  func (s) TestPickerNoRequestHash(t *testing.T) {
   180  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   181  	defer cancel()
   182  
   183  	ring, epStates := testRingAndEndpointStates([]connectivity.State{connectivity.Ready})
   184  	p := &picker{
   185  		ring:           ring,
   186  		endpointStates: epStates,
   187  	}
   188  	if _, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err == nil {
   189  		t.Errorf("Pick() should have failed with no request hash")
   190  	}
   191  }
   192  
   193  func (s) TestPickerRequestHashKey(t *testing.T) {
   194  	tests := []struct {
   195  		name         string
   196  		headerValues []string
   197  		expectedPick int
   198  	}{
   199  		{
   200  			name:         "header not set",
   201  			expectedPick: 0, // Random hash set to 0, which is within (MaxUint64 / 3 * 2, 0]
   202  		},
   203  		{
   204  			name:         "header empty",
   205  			headerValues: []string{""},
   206  			expectedPick: 0, // xxhash.Sum64String("value1,value2") is within (MaxUint64 / 3 * 2, 0]
   207  		},
   208  		{
   209  			name:         "header set to one value",
   210  			headerValues: []string{"some-value"},
   211  			expectedPick: 1, // xxhash.Sum64String("some-value") is within (0, MaxUint64 / 3]
   212  		},
   213  		{
   214  			name:         "header set to multiple values",
   215  			headerValues: []string{"value1", "value2"},
   216  			expectedPick: 2, // xxhash.Sum64String("value1,value2") is within (MaxUint64 / 3, MaxUint64 / 3 * 2]
   217  		},
   218  	}
   219  	for _, tt := range tests {
   220  		t.Run(tt.name, func(t *testing.T) {
   221  			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   222  			defer cancel()
   223  
   224  			ring, epStates := testRingAndEndpointStates(
   225  				[]connectivity.State{
   226  					connectivity.Ready,
   227  					connectivity.Ready,
   228  					connectivity.Ready,
   229  				})
   230  			headerName := "some-header"
   231  			p := &picker{
   232  				ring:              ring,
   233  				endpointStates:    epStates,
   234  				requestHashHeader: headerName,
   235  				randUint64:        func() uint64 { return 0 },
   236  			}
   237  			for _, v := range tt.headerValues {
   238  				ctx = metadata.AppendToOutgoingContext(ctx, headerName, v)
   239  			}
   240  			if res, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err != nil {
   241  				t.Errorf("Pick() failed: %v", err)
   242  			} else if res.SubConn != testSubConns[tt.expectedPick] {
   243  				t.Errorf("Pick() got = %v, want SubConn: %v", res.SubConn, testSubConns[tt.expectedPick])
   244  			}
   245  		})
   246  	}
   247  }
   248  
   249  func (s) TestPickerRandomHash(t *testing.T) {
   250  	tests := []struct {
   251  		name                         string
   252  		hash                         uint64
   253  		connectivityStates           []connectivity.State
   254  		wantSC                       balancer.SubConn
   255  		wantErr                      error
   256  		wantSCToConnect              balancer.SubConn
   257  		hasEndpointInConnectingState bool
   258  	}{
   259  		{
   260  			name:               "header not set, picked is Ready",
   261  			connectivityStates: []connectivity.State{connectivity.Ready, connectivity.Idle},
   262  			wantSC:             testSubConns[0],
   263  		},
   264  		{
   265  			name:               "header not set, picked is Idle, another is Ready. Connect and pick Ready",
   266  			connectivityStates: []connectivity.State{connectivity.Idle, connectivity.Ready},
   267  			wantSC:             testSubConns[1],
   268  			wantSCToConnect:    testSubConns[0],
   269  		},
   270  		{
   271  			name:                         "header not set, picked is Idle, there is at least one Connecting",
   272  			connectivityStates:           []connectivity.State{connectivity.Connecting, connectivity.Idle},
   273  			wantErr:                      balancer.ErrNoSubConnAvailable,
   274  			hasEndpointInConnectingState: true,
   275  		},
   276  		{
   277  			name:               "header not set, all Idle or TransientFailure, connect",
   278  			connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Idle},
   279  			wantErr:            balancer.ErrNoSubConnAvailable,
   280  			wantSCToConnect:    testSubConns[1],
   281  		},
   282  	}
   283  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   284  	defer cancel()
   285  	for _, tt := range tests {
   286  		t.Run(tt.name, func(t *testing.T) {
   287  			ring, epStates := testRingAndEndpointStates(tt.connectivityStates)
   288  			p := &picker{
   289  				ring:                         ring,
   290  				endpointStates:               epStates,
   291  				requestHashHeader:            "some-header",
   292  				hasEndpointInConnectingState: tt.hasEndpointInConnectingState,
   293  				randUint64:                   func() uint64 { return 0 }, // always return the first endpoint on the ring.
   294  			}
   295  			if got, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err != tt.wantErr {
   296  				t.Errorf("Pick() error = %v, wantErr %v", err, tt.wantErr)
   297  				return
   298  			} else if got.SubConn != tt.wantSC {
   299  				t.Errorf("Pick() got = %v, want picked SubConn: %v", got, tt.wantSC)
   300  			}
   301  			if sc := tt.wantSCToConnect; sc != nil {
   302  				select {
   303  				case <-sc.(*testutils.TestSubConn).ConnectCh:
   304  				case <-time.After(defaultTestShortTimeout):
   305  					t.Errorf("timeout waiting for Connect() from SubConn %v", sc)
   306  				}
   307  			}
   308  		})
   309  	}
   310  }