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 }