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