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 }