snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
77
balancer/ringhash/config.go
Normal file
77
balancer/ringhash/config.go
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/metadata"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMinSize = 1024
|
||||
defaultMaxSize = 4096
|
||||
ringHashSizeUpperBound = 8 * 1024 * 1024 // 8M
|
||||
)
|
||||
|
||||
func parseConfig(c json.RawMessage) (*iringhash.LBConfig, error) {
|
||||
var cfg iringhash.LBConfig
|
||||
if err := json.Unmarshal(c, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.MinRingSize > ringHashSizeUpperBound {
|
||||
return nil, fmt.Errorf("min_ring_size value of %d is greater than max supported value %d for this field", cfg.MinRingSize, ringHashSizeUpperBound)
|
||||
}
|
||||
if cfg.MaxRingSize > ringHashSizeUpperBound {
|
||||
return nil, fmt.Errorf("max_ring_size value of %d is greater than max supported value %d for this field", cfg.MaxRingSize, ringHashSizeUpperBound)
|
||||
}
|
||||
if cfg.MinRingSize == 0 {
|
||||
cfg.MinRingSize = defaultMinSize
|
||||
}
|
||||
if cfg.MaxRingSize == 0 {
|
||||
cfg.MaxRingSize = defaultMaxSize
|
||||
}
|
||||
if cfg.MinRingSize > cfg.MaxRingSize {
|
||||
return nil, fmt.Errorf("min %v is greater than max %v", cfg.MinRingSize, cfg.MaxRingSize)
|
||||
}
|
||||
if cfg.MinRingSize > envconfig.RingHashCap {
|
||||
cfg.MinRingSize = envconfig.RingHashCap
|
||||
}
|
||||
if cfg.MaxRingSize > envconfig.RingHashCap {
|
||||
cfg.MaxRingSize = envconfig.RingHashCap
|
||||
}
|
||||
if !envconfig.RingHashSetRequestHashKey {
|
||||
cfg.RequestHashHeader = ""
|
||||
}
|
||||
if cfg.RequestHashHeader != "" {
|
||||
cfg.RequestHashHeader = strings.ToLower(cfg.RequestHashHeader)
|
||||
// See rules in https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md#explicitly-setting-the-request-hash-key
|
||||
if err := metadata.ValidateKey(cfg.RequestHashHeader); err != nil {
|
||||
return nil, fmt.Errorf("invalid requestHashHeader %q: %v", cfg.RequestHashHeader, err)
|
||||
}
|
||||
if strings.HasSuffix(cfg.RequestHashHeader, "-bin") {
|
||||
return nil, fmt.Errorf("invalid requestHashHeader %q: key must not end with \"-bin\"", cfg.RequestHashHeader)
|
||||
}
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
173
balancer/ringhash/config_test.go
Normal file
173
balancer/ringhash/config_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
)
|
||||
|
||||
func (s) TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
js string
|
||||
envConfigCap uint64
|
||||
requestHeaderEnvVar bool
|
||||
want *iringhash.LBConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
js: `{"minRingSize": 1, "maxRingSize": 2}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2},
|
||||
},
|
||||
{
|
||||
name: "OK with default min",
|
||||
js: `{"maxRingSize": 2000}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: defaultMinSize, MaxRingSize: 2000},
|
||||
},
|
||||
{
|
||||
name: "OK with default max",
|
||||
js: `{"minRingSize": 2000}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 2000, MaxRingSize: defaultMaxSize},
|
||||
},
|
||||
{
|
||||
name: "min greater than max",
|
||||
js: `{"minRingSize": 10, "maxRingSize": 2}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "min greater than max greater than global limit",
|
||||
js: `{"minRingSize": 6000, "maxRingSize": 5000}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "max greater than global limit",
|
||||
js: `{"minRingSize": 1, "maxRingSize": 6000}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 4096},
|
||||
},
|
||||
{
|
||||
name: "min and max greater than global limit",
|
||||
js: `{"minRingSize": 5000, "maxRingSize": 6000}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 4096, MaxRingSize: 4096},
|
||||
},
|
||||
{
|
||||
name: "min and max less than raised global limit",
|
||||
js: `{"minRingSize": 5000, "maxRingSize": 6000}`,
|
||||
envConfigCap: 8000,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 5000, MaxRingSize: 6000},
|
||||
},
|
||||
{
|
||||
name: "min and max greater than raised global limit",
|
||||
js: `{"minRingSize": 10000, "maxRingSize": 10000}`,
|
||||
envConfigCap: 8000,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{MinRingSize: 8000, MaxRingSize: 8000},
|
||||
},
|
||||
{
|
||||
name: "min greater than upper bound",
|
||||
js: `{"minRingSize": 8388610, "maxRingSize": 10}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "max greater than upper bound",
|
||||
js: `{"minRingSize": 10, "maxRingSize": 8388610}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "request metadata key set",
|
||||
js: `{"requestHashHeader": "x-foo"}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{
|
||||
MinRingSize: defaultMinSize,
|
||||
MaxRingSize: defaultMaxSize,
|
||||
RequestHashHeader: "x-foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request metadata key set with uppercase letters",
|
||||
js: `{"requestHashHeader": "x-FOO"}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: &iringhash.LBConfig{
|
||||
MinRingSize: defaultMinSize,
|
||||
MaxRingSize: defaultMaxSize,
|
||||
RequestHashHeader: "x-foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid request hash header",
|
||||
js: `{"requestHashHeader": "!invalid"}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "binary request hash header",
|
||||
js: `{"requestHashHeader": "header-with-bin"}`,
|
||||
requestHeaderEnvVar: true,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "request hash header cleared when RingHashSetRequestHashKey env var is false",
|
||||
js: `{"requestHashHeader": "x-foo"}`,
|
||||
requestHeaderEnvVar: false,
|
||||
want: &iringhash.LBConfig{
|
||||
MinRingSize: defaultMinSize,
|
||||
MaxRingSize: defaultMaxSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.envConfigCap != 0 {
|
||||
testutils.SetEnvConfig(t, &envconfig.RingHashCap, tt.envConfigCap)
|
||||
}
|
||||
testutils.SetEnvConfig(t, &envconfig.RingHashSetRequestHashKey, tt.requestHeaderEnvVar)
|
||||
got, err := parseConfig(json.RawMessage(tt.js))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want); diff != "" {
|
||||
t.Errorf("parseConfig() got unexpected output, diff (-got +want): %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
34
balancer/ringhash/logging.go
Normal file
34
balancer/ringhash/logging.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
const prefix = "[ring-hash-lb %p] "
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
func prefixLogger(p *ringhashBalancer) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
|
||||
}
|
||||
124
balancer/ringhash/picker.go
Normal file
124
balancer/ringhash/picker.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type picker struct {
|
||||
ring *ring
|
||||
|
||||
// endpointStates is a cache of endpoint states.
|
||||
// The ringhash balancer stores endpoint states in a `resolver.EndpointMap`,
|
||||
// with access guarded by `ringhashBalancer.mu`. The `endpointStates` cache
|
||||
// in the picker helps avoid locking the ringhash balancer's mutex when
|
||||
// reading the latest state at RPC time.
|
||||
endpointStates map[string]endpointState // endpointState.hashKey -> endpointState
|
||||
|
||||
// requestHashHeader is the header key to look for the request hash. If it's
|
||||
// empty, the request hash is expected to be set in the context via xDS.
|
||||
// See gRFC A76.
|
||||
requestHashHeader string
|
||||
|
||||
// hasEndpointInConnectingState is true if any of the endpoints is in
|
||||
// CONNECTING.
|
||||
hasEndpointInConnectingState bool
|
||||
|
||||
randUint64 func() uint64
|
||||
}
|
||||
|
||||
func (p *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
|
||||
usingRandomHash := false
|
||||
var requestHash uint64
|
||||
if p.requestHashHeader == "" {
|
||||
var ok bool
|
||||
if requestHash, ok = iringhash.XDSRequestHash(info.Ctx); !ok {
|
||||
return balancer.PickResult{}, fmt.Errorf("ringhash: expected xDS config selector to set the request hash")
|
||||
}
|
||||
} else {
|
||||
md, ok := metadata.FromOutgoingContext(info.Ctx)
|
||||
if !ok || len(md.Get(p.requestHashHeader)) == 0 {
|
||||
requestHash = p.randUint64()
|
||||
usingRandomHash = true
|
||||
} else {
|
||||
values := strings.Join(md.Get(p.requestHashHeader), ",")
|
||||
requestHash = xxhash.Sum64String(values)
|
||||
}
|
||||
}
|
||||
|
||||
e := p.ring.pick(requestHash)
|
||||
ringSize := len(p.ring.items)
|
||||
if !usingRandomHash {
|
||||
// Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF,
|
||||
// we ignore all TF subchannels and find the first ring entry in READY,
|
||||
// CONNECTING or IDLE. If that entry is in IDLE, we need to initiate a
|
||||
// connection. The idlePicker returned by the LazyLB or the new Pickfirst
|
||||
// should do this automatically.
|
||||
for i := 0; i < ringSize; i++ {
|
||||
index := (e.idx + i) % ringSize
|
||||
es := p.endpointState(p.ring.items[index])
|
||||
switch es.state.ConnectivityState {
|
||||
case connectivity.Ready, connectivity.Connecting, connectivity.Idle:
|
||||
return es.state.Picker.Pick(info)
|
||||
case connectivity.TransientFailure:
|
||||
default:
|
||||
panic(fmt.Sprintf("Found child balancer in unknown state: %v", es.state.ConnectivityState))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the picker has generated a random hash, it will walk the ring from
|
||||
// this hash, and pick the first READY endpoint. If no endpoint is
|
||||
// currently in CONNECTING state, it will trigger a connection attempt
|
||||
// on at most one endpoint that is in IDLE state along the way. - A76
|
||||
requestedConnection := p.hasEndpointInConnectingState
|
||||
for i := 0; i < ringSize; i++ {
|
||||
index := (e.idx + i) % ringSize
|
||||
es := p.endpointState(p.ring.items[index])
|
||||
if es.state.ConnectivityState == connectivity.Ready {
|
||||
return es.state.Picker.Pick(info)
|
||||
}
|
||||
if !requestedConnection && es.state.ConnectivityState == connectivity.Idle {
|
||||
requestedConnection = true
|
||||
// If the SubChannel is in idle state, initiate a connection but
|
||||
// continue to check other pickers to see if there is one in
|
||||
// ready state.
|
||||
es.exitIdle()
|
||||
}
|
||||
}
|
||||
if requestedConnection {
|
||||
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
|
||||
}
|
||||
}
|
||||
|
||||
// All children are in transient failure. Return the first failure.
|
||||
return p.endpointState(e).state.Picker.Pick(info)
|
||||
}
|
||||
|
||||
func (p *picker) endpointState(e *ringEntry) endpointState {
|
||||
return p.endpointStates[e.hashKey]
|
||||
}
|
||||
301
balancer/ringhash/picker_test.go
Normal file
301
balancer/ringhash/picker_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
testSubConns []*testutils.TestSubConn
|
||||
errPicker = errors.New("picker in TransientFailure")
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 8; i++ {
|
||||
testSubConns = append(testSubConns, testutils.NewTestSubConn(fmt.Sprint(i)))
|
||||
}
|
||||
}
|
||||
|
||||
// fakeChildPicker is used to mock pickers from child pickfirst balancers.
|
||||
type fakeChildPicker struct {
|
||||
connectivityState connectivity.State
|
||||
subConn *testutils.TestSubConn
|
||||
tfError error
|
||||
}
|
||||
|
||||
func (p *fakeChildPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
|
||||
switch p.connectivityState {
|
||||
case connectivity.Idle:
|
||||
p.subConn.Connect()
|
||||
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
|
||||
case connectivity.Connecting:
|
||||
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
|
||||
case connectivity.Ready:
|
||||
return balancer.PickResult{SubConn: p.subConn}, nil
|
||||
default:
|
||||
return balancer.PickResult{}, p.tfError
|
||||
}
|
||||
}
|
||||
|
||||
func testRingAndEndpointStates(states []connectivity.State) (*ring, map[string]endpointState) {
|
||||
var items []*ringEntry
|
||||
epStates := map[string]endpointState{}
|
||||
for i, st := range states {
|
||||
testSC := testSubConns[i]
|
||||
items = append(items, &ringEntry{
|
||||
idx: i,
|
||||
hash: math.MaxUint64 / uint64(len(states)) * uint64(i),
|
||||
hashKey: testSC.String(),
|
||||
})
|
||||
epState := endpointState{
|
||||
state: balancer.State{
|
||||
ConnectivityState: st,
|
||||
Picker: &fakeChildPicker{
|
||||
connectivityState: st,
|
||||
tfError: fmt.Errorf("%d: %w", i, errPicker),
|
||||
subConn: testSC,
|
||||
},
|
||||
},
|
||||
exitIdle: func() { testSC.Connect() },
|
||||
}
|
||||
epStates[testSC.String()] = epState
|
||||
}
|
||||
return &ring{items: items}, epStates
|
||||
}
|
||||
|
||||
func (s) TestPickerPickFirstTwo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
connectivityStates []connectivity.State
|
||||
wantSC balancer.SubConn
|
||||
wantErr error
|
||||
wantSCToConnect balancer.SubConn
|
||||
}{
|
||||
{
|
||||
name: "picked is Ready",
|
||||
connectivityStates: []connectivity.State{connectivity.Ready, connectivity.Idle},
|
||||
wantSC: testSubConns[0],
|
||||
},
|
||||
{
|
||||
name: "picked is connecting, queue",
|
||||
connectivityStates: []connectivity.State{connectivity.Connecting, connectivity.Idle},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
},
|
||||
{
|
||||
name: "picked is Idle, connect and queue",
|
||||
connectivityStates: []connectivity.State{connectivity.Idle, connectivity.Idle},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
wantSCToConnect: testSubConns[0],
|
||||
},
|
||||
{
|
||||
name: "picked is TransientFailure, next is ready, return",
|
||||
connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Ready},
|
||||
wantSC: testSubConns[1],
|
||||
},
|
||||
{
|
||||
name: "picked is TransientFailure, next is connecting, queue",
|
||||
connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Connecting},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
},
|
||||
{
|
||||
name: "picked is TransientFailure, next is Idle, connect and queue",
|
||||
connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Idle},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
wantSCToConnect: testSubConns[1],
|
||||
},
|
||||
{
|
||||
name: "all are in TransientFailure, return picked failure",
|
||||
connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.TransientFailure},
|
||||
wantErr: errPicker,
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ring, epStates := testRingAndEndpointStates(tt.connectivityStates)
|
||||
p := &picker{
|
||||
ring: ring,
|
||||
endpointStates: epStates,
|
||||
}
|
||||
got, err := p.Pick(balancer.PickInfo{
|
||||
Ctx: iringhash.SetXDSRequestHash(ctx, 0), // always pick the first endpoint on the ring.
|
||||
})
|
||||
if (err != nil || tt.wantErr != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Pick() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got.SubConn != tt.wantSC {
|
||||
t.Errorf("Pick() got = %v, want picked SubConn: %v", got, tt.wantSC)
|
||||
}
|
||||
if sc := tt.wantSCToConnect; sc != nil {
|
||||
select {
|
||||
case <-sc.(*testutils.TestSubConn).ConnectCh:
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestPickerNoRequestHash(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ring, epStates := testRingAndEndpointStates([]connectivity.State{connectivity.Ready})
|
||||
p := &picker{
|
||||
ring: ring,
|
||||
endpointStates: epStates,
|
||||
}
|
||||
if _, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err == nil {
|
||||
t.Errorf("Pick() should have failed with no request hash")
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestPickerRequestHashKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headerValues []string
|
||||
expectedPick int
|
||||
}{
|
||||
{
|
||||
name: "header not set",
|
||||
expectedPick: 0, // Random hash set to 0, which is within (MaxUint64 / 3 * 2, 0]
|
||||
},
|
||||
{
|
||||
name: "header empty",
|
||||
headerValues: []string{""},
|
||||
expectedPick: 0, // xxhash.Sum64String("value1,value2") is within (MaxUint64 / 3 * 2, 0]
|
||||
},
|
||||
{
|
||||
name: "header set to one value",
|
||||
headerValues: []string{"some-value"},
|
||||
expectedPick: 1, // xxhash.Sum64String("some-value") is within (0, MaxUint64 / 3]
|
||||
},
|
||||
{
|
||||
name: "header set to multiple values",
|
||||
headerValues: []string{"value1", "value2"},
|
||||
expectedPick: 2, // xxhash.Sum64String("value1,value2") is within (MaxUint64 / 3, MaxUint64 / 3 * 2]
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ring, epStates := testRingAndEndpointStates(
|
||||
[]connectivity.State{
|
||||
connectivity.Ready,
|
||||
connectivity.Ready,
|
||||
connectivity.Ready,
|
||||
})
|
||||
headerName := "some-header"
|
||||
p := &picker{
|
||||
ring: ring,
|
||||
endpointStates: epStates,
|
||||
requestHashHeader: headerName,
|
||||
randUint64: func() uint64 { return 0 },
|
||||
}
|
||||
for _, v := range tt.headerValues {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, headerName, v)
|
||||
}
|
||||
if res, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err != nil {
|
||||
t.Errorf("Pick() failed: %v", err)
|
||||
} else if res.SubConn != testSubConns[tt.expectedPick] {
|
||||
t.Errorf("Pick() got = %v, want SubConn: %v", res.SubConn, testSubConns[tt.expectedPick])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestPickerRandomHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash uint64
|
||||
connectivityStates []connectivity.State
|
||||
wantSC balancer.SubConn
|
||||
wantErr error
|
||||
wantSCToConnect balancer.SubConn
|
||||
hasEndpointInConnectingState bool
|
||||
}{
|
||||
{
|
||||
name: "header not set, picked is Ready",
|
||||
connectivityStates: []connectivity.State{connectivity.Ready, connectivity.Idle},
|
||||
wantSC: testSubConns[0],
|
||||
},
|
||||
{
|
||||
name: "header not set, picked is Idle, another is Ready. Connect and pick Ready",
|
||||
connectivityStates: []connectivity.State{connectivity.Idle, connectivity.Ready},
|
||||
wantSC: testSubConns[1],
|
||||
wantSCToConnect: testSubConns[0],
|
||||
},
|
||||
{
|
||||
name: "header not set, picked is Idle, there is at least one Connecting",
|
||||
connectivityStates: []connectivity.State{connectivity.Connecting, connectivity.Idle},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
hasEndpointInConnectingState: true,
|
||||
},
|
||||
{
|
||||
name: "header not set, all Idle or TransientFailure, connect",
|
||||
connectivityStates: []connectivity.State{connectivity.TransientFailure, connectivity.Idle},
|
||||
wantErr: balancer.ErrNoSubConnAvailable,
|
||||
wantSCToConnect: testSubConns[1],
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ring, epStates := testRingAndEndpointStates(tt.connectivityStates)
|
||||
p := &picker{
|
||||
ring: ring,
|
||||
endpointStates: epStates,
|
||||
requestHashHeader: "some-header",
|
||||
hasEndpointInConnectingState: tt.hasEndpointInConnectingState,
|
||||
randUint64: func() uint64 { return 0 }, // always return the first endpoint on the ring.
|
||||
}
|
||||
if got, err := p.Pick(balancer.PickInfo{Ctx: ctx}); err != tt.wantErr {
|
||||
t.Errorf("Pick() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
} else if got.SubConn != tt.wantSC {
|
||||
t.Errorf("Pick() got = %v, want picked SubConn: %v", got, tt.wantSC)
|
||||
}
|
||||
if sc := tt.wantSCToConnect; sc != nil {
|
||||
select {
|
||||
case <-sc.(*testutils.TestSubConn).ConnectCh:
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
189
balancer/ringhash/ring.go
Normal file
189
balancer/ringhash/ring.go
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type ring struct {
|
||||
items []*ringEntry
|
||||
}
|
||||
|
||||
type endpointInfo struct {
|
||||
hashKey string
|
||||
scaledWeight float64
|
||||
originalWeight uint32
|
||||
}
|
||||
|
||||
type ringEntry struct {
|
||||
idx int
|
||||
hash uint64
|
||||
hashKey string
|
||||
weight uint32
|
||||
}
|
||||
|
||||
// newRing creates a ring from the endpoints stored in the EndpointMap. The ring
|
||||
// size is limited by the passed in max/min.
|
||||
//
|
||||
// ring entries will be created for each endpoint, and endpoints with high
|
||||
// weight (specified by the endpoint) may have multiple entries.
|
||||
//
|
||||
// For example, for endpoints with weights {a:3, b:3, c:4}, a generated ring of
|
||||
// size 10 could be:
|
||||
// - {idx:0 hash:3689675255460411075 b}
|
||||
// - {idx:1 hash:4262906501694543955 c}
|
||||
// - {idx:2 hash:5712155492001633497 c}
|
||||
// - {idx:3 hash:8050519350657643659 b}
|
||||
// - {idx:4 hash:8723022065838381142 b}
|
||||
// - {idx:5 hash:11532782514799973195 a}
|
||||
// - {idx:6 hash:13157034721563383607 c}
|
||||
// - {idx:7 hash:14468677667651225770 c}
|
||||
// - {idx:8 hash:17336016884672388720 a}
|
||||
// - {idx:9 hash:18151002094784932496 a}
|
||||
//
|
||||
// To pick from a ring, a binary search will be done for the given target hash,
|
||||
// and first item with hash >= given hash will be returned.
|
||||
//
|
||||
// Must be called with a non-empty endpoints map.
|
||||
func newRing(endpoints *resolver.EndpointMap[*endpointState], minRingSize, maxRingSize uint64, logger *grpclog.PrefixLogger) *ring {
|
||||
if logger.V(2) {
|
||||
logger.Infof("newRing: number of endpoints is %d, minRingSize is %d, maxRingSize is %d", endpoints.Len(), minRingSize, maxRingSize)
|
||||
}
|
||||
|
||||
// https://github.com/envoyproxy/envoy/blob/765c970f06a4c962961a0e03a467e165b276d50f/source/common/upstream/ring_hash_lb.cc#L114
|
||||
normalizedWeights, minWeight := normalizeWeights(endpoints)
|
||||
if logger.V(2) {
|
||||
logger.Infof("newRing: normalized endpoint weights is %v", normalizedWeights)
|
||||
}
|
||||
|
||||
// Normalized weights for {3,3,4} is {0.3,0.3,0.4}.
|
||||
|
||||
// Scale up the size of the ring such that the least-weighted host gets a
|
||||
// whole number of hashes on the ring.
|
||||
//
|
||||
// Note that size is limited by the input max/min.
|
||||
scale := math.Min(math.Ceil(minWeight*float64(minRingSize))/minWeight, float64(maxRingSize))
|
||||
ringSize := math.Ceil(scale)
|
||||
items := make([]*ringEntry, 0, int(ringSize))
|
||||
if logger.V(2) {
|
||||
logger.Infof("newRing: creating new ring of size %v", ringSize)
|
||||
}
|
||||
|
||||
// For each entry, scale*weight nodes are generated in the ring.
|
||||
//
|
||||
// Not all of these are whole numbers. E.g. for weights {a:3,b:3,c:4}, if
|
||||
// ring size is 7, scale is 6.66. The numbers of nodes will be
|
||||
// {a,a,b,b,c,c,c}.
|
||||
//
|
||||
// A hash is generated for each item, and later the results will be sorted
|
||||
// based on the hash.
|
||||
var currentHashes, targetHashes float64
|
||||
for _, epInfo := range normalizedWeights {
|
||||
targetHashes += scale * epInfo.scaledWeight
|
||||
// This index ensures that ring entries corresponding to the same
|
||||
// endpoint hash to different values. And since this index is
|
||||
// per-endpoint, these entries hash to the same value across address
|
||||
// updates.
|
||||
idx := 0
|
||||
for currentHashes < targetHashes {
|
||||
h := xxhash.Sum64String(epInfo.hashKey + "_" + strconv.Itoa(idx))
|
||||
items = append(items, &ringEntry{hash: h, hashKey: epInfo.hashKey, weight: epInfo.originalWeight})
|
||||
idx++
|
||||
currentHashes++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort items based on hash, to prepare for binary search.
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].hash < items[j].hash })
|
||||
for i, ii := range items {
|
||||
ii.idx = i
|
||||
}
|
||||
return &ring{items: items}
|
||||
}
|
||||
|
||||
// normalizeWeights calculates the normalized weights for each endpoint in the
|
||||
// given endpoints map. It returns a slice of endpointWithState structs, where
|
||||
// each struct contains the picker for an endpoint and its corresponding weight.
|
||||
// The function also returns the minimum weight among all endpoints.
|
||||
//
|
||||
// The normalized weight of each endpoint is calculated by dividing its weight
|
||||
// attribute by the sum of all endpoint weights. If the weight attribute is not
|
||||
// found on the endpoint, a default weight of 1 is used.
|
||||
//
|
||||
// The endpoints are sorted in ascending order to ensure consistent results.
|
||||
//
|
||||
// Must be called with a non-empty endpoints map.
|
||||
func normalizeWeights(endpoints *resolver.EndpointMap[*endpointState]) ([]endpointInfo, float64) {
|
||||
var weightSum uint32
|
||||
// Since attributes are explicitly ignored in the EndpointMap key, we need
|
||||
// to iterate over the values to get the weights.
|
||||
endpointVals := endpoints.Values()
|
||||
for _, epState := range endpointVals {
|
||||
weightSum += epState.weight
|
||||
}
|
||||
ret := make([]endpointInfo, 0, endpoints.Len())
|
||||
min := 1.0
|
||||
for _, epState := range endpointVals {
|
||||
// (*endpointState).weight is set to 1 if the weight attribute is not
|
||||
// found on the endpoint. And since this function is guaranteed to be
|
||||
// called with a non-empty endpoints map, weightSum is guaranteed to be
|
||||
// non-zero. So, we need not worry about divide by zero error here.
|
||||
nw := float64(epState.weight) / float64(weightSum)
|
||||
ret = append(ret, endpointInfo{
|
||||
hashKey: epState.hashKey,
|
||||
scaledWeight: nw,
|
||||
originalWeight: epState.weight,
|
||||
})
|
||||
min = math.Min(min, nw)
|
||||
}
|
||||
// Sort the endpoints to return consistent results.
|
||||
//
|
||||
// Note: this might not be necessary, but this makes sure the ring is
|
||||
// consistent as long as the endpoints are the same, for example, in cases
|
||||
// where an endpoint is added and then removed, the RPCs will still pick the
|
||||
// same old endpoint.
|
||||
sort.Slice(ret, func(i, j int) bool {
|
||||
return ret[i].hashKey < ret[j].hashKey
|
||||
})
|
||||
return ret, min
|
||||
}
|
||||
|
||||
// pick does a binary search. It returns the item with smallest index i that
|
||||
// r.items[i].hash >= h.
|
||||
func (r *ring) pick(h uint64) *ringEntry {
|
||||
i := sort.Search(len(r.items), func(i int) bool { return r.items[i].hash >= h })
|
||||
if i == len(r.items) {
|
||||
// If not found, and h is greater than the largest hash, return the
|
||||
// first item.
|
||||
i = 0
|
||||
}
|
||||
return r.items[i]
|
||||
}
|
||||
|
||||
// next returns the next entry.
|
||||
func (r *ring) next(e *ringEntry) *ringEntry {
|
||||
return r.items[(e.idx+1)%len(r.items)]
|
||||
}
|
||||
112
balancer/ringhash/ring_test.go
Normal file
112
balancer/ringhash/ring_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
"google.golang.org/grpc/experimental/balancer/weight"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
var testEndpoints []resolver.Endpoint
|
||||
var testEndpointStateMap *resolver.EndpointMap[*endpointState]
|
||||
|
||||
func init() {
|
||||
testEndpoints = []resolver.Endpoint{
|
||||
testEndpoint("a", 3),
|
||||
testEndpoint("b", 3),
|
||||
testEndpoint("c", 4),
|
||||
}
|
||||
testEndpointStateMap = resolver.NewEndpointMap[*endpointState]()
|
||||
testEndpointStateMap.Set(testEndpoints[0], &endpointState{hashKey: "a", weight: 3})
|
||||
testEndpointStateMap.Set(testEndpoints[1], &endpointState{hashKey: "b", weight: 3})
|
||||
testEndpointStateMap.Set(testEndpoints[2], &endpointState{hashKey: "c", weight: 4})
|
||||
}
|
||||
|
||||
func testEndpoint(addr string, endpointWeight uint32) resolver.Endpoint {
|
||||
ep := resolver.Endpoint{Addresses: []resolver.Address{{Addr: addr}}}
|
||||
return weight.Set(ep, weight.EndpointInfo{Weight: endpointWeight})
|
||||
}
|
||||
|
||||
func (s) TestRingNew(t *testing.T) {
|
||||
var totalWeight float64 = 10
|
||||
for _, min := range []uint64{3, 4, 6, 8} {
|
||||
for _, max := range []uint64{20, 8} {
|
||||
t.Run(fmt.Sprintf("size-min-%v-max-%v", min, max), func(t *testing.T) {
|
||||
r := newRing(testEndpointStateMap, min, max, nil)
|
||||
totalCount := len(r.items)
|
||||
if totalCount < int(min) || totalCount > int(max) {
|
||||
t.Fatalf("unexpected size %v, want min %v, max %v", totalCount, min, max)
|
||||
}
|
||||
for _, e := range testEndpoints {
|
||||
var count int
|
||||
for _, ii := range r.items {
|
||||
if ii.hashKey == hashKey(e) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
got := float64(count) / float64(totalCount)
|
||||
want := float64(getWeightAttribute(e)) / totalWeight
|
||||
if !equalApproximately(got, want) {
|
||||
t.Fatalf("unexpected item weight in ring: %v != %v", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalApproximately(x, y float64) bool {
|
||||
delta := math.Abs(x - y)
|
||||
mean := math.Abs(x+y) / 2.0
|
||||
return delta/mean < 0.25
|
||||
}
|
||||
|
||||
func (s) TestRingPick(t *testing.T) {
|
||||
r := newRing(testEndpointStateMap, 10, 20, nil)
|
||||
for _, h := range []uint64{xxhash.Sum64String("1"), xxhash.Sum64String("2"), xxhash.Sum64String("3"), xxhash.Sum64String("4")} {
|
||||
t.Run(fmt.Sprintf("picking-hash-%v", h), func(t *testing.T) {
|
||||
e := r.pick(h)
|
||||
var low uint64
|
||||
if e.idx > 0 {
|
||||
low = r.items[e.idx-1].hash
|
||||
}
|
||||
high := e.hash
|
||||
// h should be in [low, high).
|
||||
if h < low || h >= high {
|
||||
t.Fatalf("unexpected item picked, hash: %v, low: %v, high: %v", h, low, high)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestRingNext(t *testing.T) {
|
||||
r := newRing(testEndpointStateMap, 10, 20, nil)
|
||||
|
||||
for _, e := range r.items {
|
||||
ne := r.next(e)
|
||||
if ne.idx != (e.idx+1)%len(r.items) {
|
||||
t.Fatalf("next(%+v) returned unexpected %+v", e, ne)
|
||||
}
|
||||
}
|
||||
}
|
||||
410
balancer/ringhash/ringhash.go
Normal file
410
balancer/ringhash/ringhash.go
Normal file
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package ringhash implements the ringhash balancer. See the following
|
||||
// gRFCs for details:
|
||||
// - https://github.com/grpc/proposal/blob/master/A42-xds-ring-hash-lb-policy.md
|
||||
// - https://github.com/grpc/proposal/blob/master/A61-IPv4-IPv6-dualstack-backends.md#ring-hash
|
||||
// - https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This package is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/base"
|
||||
"google.golang.org/grpc/balancer/endpointsharding"
|
||||
"google.golang.org/grpc/balancer/lazy"
|
||||
"google.golang.org/grpc/balancer/pickfirst"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/experimental/balancer/weight"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/ringhash"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
// Name is the name of the ring_hash balancer.
|
||||
const Name = "ring_hash_experimental"
|
||||
|
||||
func lazyPickFirstBuilder(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
return lazy.NewBalancer(cc, opts, balancer.Get(pickfirst.Name).Build)
|
||||
}
|
||||
|
||||
func init() {
|
||||
balancer.Register(bb{})
|
||||
}
|
||||
|
||||
type bb struct{}
|
||||
|
||||
func (bb) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
b := &ringhashBalancer{
|
||||
ClientConn: cc,
|
||||
endpointStates: resolver.NewEndpointMap[*endpointState](),
|
||||
}
|
||||
esOpts := endpointsharding.Options{DisableAutoReconnect: true}
|
||||
b.child = endpointsharding.NewBalancer(b, opts, lazyPickFirstBuilder, esOpts)
|
||||
b.logger = prefixLogger(b)
|
||||
b.logger.Infof("Created")
|
||||
return b
|
||||
}
|
||||
|
||||
func (bb) Name() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
return parseConfig(c)
|
||||
}
|
||||
|
||||
type ringhashBalancer struct {
|
||||
// The following fields are initialized at build time and read-only after
|
||||
// that and therefore do not need to be guarded by a mutex.
|
||||
|
||||
// ClientConn is embedded to intercept UpdateState calls from the child
|
||||
// endpointsharding balancer.
|
||||
balancer.ClientConn
|
||||
logger *grpclog.PrefixLogger
|
||||
child balancer.Balancer
|
||||
|
||||
mu sync.Mutex
|
||||
config *iringhash.LBConfig
|
||||
inhibitChildUpdates bool
|
||||
shouldRegenerateRing bool
|
||||
endpointStates *resolver.EndpointMap[*endpointState]
|
||||
|
||||
// ring is always in sync with endpoints. When endpoints change, a new ring
|
||||
// is generated. Note that address weights updates also regenerates the
|
||||
// ring.
|
||||
ring *ring
|
||||
}
|
||||
|
||||
// hashKey returns the hash key to use for an endpoint. Per gRFC A61, each entry
|
||||
// in the ring is a hash of the endpoint's hash key concatenated with a
|
||||
// per-entry unique suffix.
|
||||
func hashKey(endpoint resolver.Endpoint) string {
|
||||
if hk := ringhash.HashKey(endpoint); hk != "" {
|
||||
return hk
|
||||
}
|
||||
// If no hash key is set, use the endpoint's first address as the hash key.
|
||||
// This is the default behavior when no hash key is set.
|
||||
return endpoint.Addresses[0].Addr
|
||||
}
|
||||
|
||||
// UpdateState intercepts child balancer state updates. It updates the
|
||||
// per-endpoint state stored in the ring, and also the aggregated state based on
|
||||
// the child picker. It also reconciles the endpoint list. It sets
|
||||
// `b.shouldRegenerateRing` to true if the new endpoint list is different from
|
||||
// the previous, i.e. any of the following is true:
|
||||
// - an endpoint was added
|
||||
// - an endpoint was removed
|
||||
// - an endpoint's weight was updated
|
||||
// - the first addresses of the endpoint has changed
|
||||
func (b *ringhashBalancer) UpdateState(state balancer.State) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
childStates := endpointsharding.ChildStatesFromPicker(state.Picker)
|
||||
// endpointsSet is the set converted from endpoints, used for quick lookup.
|
||||
endpointsSet := resolver.NewEndpointMap[bool]()
|
||||
|
||||
for _, childState := range childStates {
|
||||
endpoint := childState.Endpoint
|
||||
endpointsSet.Set(endpoint, true)
|
||||
newWeight := getWeightAttribute(endpoint)
|
||||
hk := hashKey(endpoint)
|
||||
es, ok := b.endpointStates.Get(endpoint)
|
||||
if !ok {
|
||||
es := &endpointState{
|
||||
hashKey: hk,
|
||||
weight: newWeight,
|
||||
state: childState.State,
|
||||
exitIdle: childState.ExitIdle,
|
||||
}
|
||||
b.endpointStates.Set(endpoint, es)
|
||||
b.shouldRegenerateRing = true
|
||||
} else {
|
||||
// We have seen this endpoint before and created a `endpointState`
|
||||
// object for it. If the weight or the hash key of the endpoint has
|
||||
// changed, update the endpoint state map with the new weight or
|
||||
// hash key. This will be used when a new ring is created.
|
||||
if oldWeight := es.weight; oldWeight != newWeight {
|
||||
b.shouldRegenerateRing = true
|
||||
es.weight = newWeight
|
||||
}
|
||||
if es.hashKey != hk {
|
||||
b.shouldRegenerateRing = true
|
||||
es.hashKey = hk
|
||||
}
|
||||
es.state = childState.State
|
||||
}
|
||||
}
|
||||
|
||||
for endpoint := range b.endpointStates.All() {
|
||||
if _, ok := endpointsSet.Get(endpoint); ok {
|
||||
continue
|
||||
}
|
||||
// endpoint was removed by resolver.
|
||||
b.endpointStates.Delete(endpoint)
|
||||
b.shouldRegenerateRing = true
|
||||
}
|
||||
|
||||
b.updatePickerLocked()
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) UpdateClientConnState(ccs balancer.ClientConnState) error {
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Received update from resolver, balancer config: %+v", pretty.ToJSON(ccs.BalancerConfig))
|
||||
}
|
||||
|
||||
newConfig, ok := ccs.BalancerConfig.(*iringhash.LBConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected balancer config with type: %T", ccs.BalancerConfig)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.inhibitChildUpdates = true
|
||||
b.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
b.mu.Lock()
|
||||
b.inhibitChildUpdates = false
|
||||
b.updatePickerLocked()
|
||||
b.mu.Unlock()
|
||||
}()
|
||||
|
||||
if err := b.child.UpdateClientConnState(balancer.ClientConnState{
|
||||
// Make pickfirst children use health listeners for outlier detection
|
||||
// and health checking to work.
|
||||
ResolverState: pickfirst.EnableHealthListener(ccs.ResolverState),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
// Ring updates can happen due to the following:
|
||||
// 1. Addition or deletion of endpoints: The synchronous picker update from
|
||||
// the child endpointsharding balancer would contain the list of updated
|
||||
// endpoints. Updates triggered by the child after handling the
|
||||
// `UpdateClientConnState` call will not change the endpoint list.
|
||||
// 2. Change in the `LoadBalancerConfig`: Ring config such as max/min ring
|
||||
// size.
|
||||
// To avoid extra ring updates, a boolean is used to track the need for a
|
||||
// ring update and the update is done only once at the end.
|
||||
//
|
||||
// If the ring configuration has changed, we need to regenerate the ring
|
||||
// while sending a new picker.
|
||||
if b.config == nil || b.config.MinRingSize != newConfig.MinRingSize || b.config.MaxRingSize != newConfig.MaxRingSize {
|
||||
b.shouldRegenerateRing = true
|
||||
}
|
||||
b.config = newConfig
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) ResolverError(err error) {
|
||||
b.child.ResolverError(err)
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
|
||||
b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) updatePickerLocked() {
|
||||
state := b.aggregatedStateLocked()
|
||||
// Start connecting to new endpoints if necessary.
|
||||
if state == connectivity.Connecting || state == connectivity.TransientFailure {
|
||||
// When overall state is TransientFailure, we need to make sure at least
|
||||
// one endpoint is attempting to connect, otherwise this balancer may
|
||||
// never get picks if the parent is priority.
|
||||
//
|
||||
// Because we report Connecting as the overall state when only one
|
||||
// endpoint is in TransientFailure, we do the same check for Connecting
|
||||
// here.
|
||||
//
|
||||
// Note that this check also covers deleting endpoints. E.g. if the
|
||||
// endpoint attempting to connect is deleted, and the overall state is
|
||||
// TF. Since there must be at least one endpoint attempting to connect,
|
||||
// we need to trigger one.
|
||||
//
|
||||
// After calling `ExitIdle` on a child balancer, the child will send a
|
||||
// picker update asynchronously. A race condition may occur if another
|
||||
// picker update from endpointsharding arrives before the child's
|
||||
// picker update. The received picker may trigger a re-execution of the
|
||||
// loop below to find an idle child. Since map iteration order is
|
||||
// non-deterministic, the list of `endpointState`s must be sorted to
|
||||
// ensure `ExitIdle` is called on the same child, preventing unnecessary
|
||||
// connections.
|
||||
endpointStates := make([]*endpointState, 0, b.endpointStates.Len())
|
||||
for _, s := range b.endpointStates.All() {
|
||||
endpointStates = append(endpointStates, s)
|
||||
}
|
||||
sort.Slice(endpointStates, func(i, j int) bool {
|
||||
return endpointStates[i].hashKey < endpointStates[j].hashKey
|
||||
})
|
||||
|
||||
// Store the function to ExitIdle on the first IDLE endpoint.
|
||||
var exitIdle func()
|
||||
for _, es := range endpointStates {
|
||||
connState := es.state.ConnectivityState
|
||||
if connState == connectivity.Connecting {
|
||||
exitIdle = nil
|
||||
break
|
||||
}
|
||||
if exitIdle == nil && connState == connectivity.Idle {
|
||||
exitIdle = es.exitIdle
|
||||
}
|
||||
}
|
||||
if exitIdle != nil {
|
||||
exitIdle()
|
||||
}
|
||||
}
|
||||
|
||||
if b.inhibitChildUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the channel.
|
||||
if b.endpointStates.Len() > 0 && b.shouldRegenerateRing {
|
||||
// with a non-empty list of endpoints.
|
||||
b.ring = newRing(b.endpointStates, b.config.MinRingSize, b.config.MaxRingSize, b.logger)
|
||||
}
|
||||
b.shouldRegenerateRing = false
|
||||
var newPicker balancer.Picker
|
||||
if b.endpointStates.Len() == 0 {
|
||||
newPicker = base.NewErrPicker(errors.New("produced zero addresses"))
|
||||
} else {
|
||||
newPicker = b.newPickerLocked()
|
||||
}
|
||||
b.ClientConn.UpdateState(balancer.State{
|
||||
ConnectivityState: state,
|
||||
Picker: newPicker,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) Close() {
|
||||
b.logger.Infof("Shutdown")
|
||||
b.child.Close()
|
||||
}
|
||||
|
||||
func (b *ringhashBalancer) ExitIdle() {
|
||||
// ExitIdle implementation is a no-op because connections are either
|
||||
// triggers from picks or from child balancer state changes.
|
||||
}
|
||||
|
||||
// newPickerLocked generates a picker. The picker copies the endpoint states
|
||||
// over to avoid locking the mutex at RPC time. The picker should be
|
||||
// re-generated every time an endpoint state is updated.
|
||||
func (b *ringhashBalancer) newPickerLocked() *picker {
|
||||
states := make(map[string]endpointState)
|
||||
hasEndpointConnecting := false
|
||||
for _, epState := range b.endpointStates.All() {
|
||||
// Copy the endpoint state to avoid races, since ring hash
|
||||
// mutates the state, weight and hash key in place.
|
||||
states[epState.hashKey] = *epState
|
||||
if epState.state.ConnectivityState == connectivity.Connecting {
|
||||
hasEndpointConnecting = true
|
||||
}
|
||||
}
|
||||
return &picker{
|
||||
ring: b.ring,
|
||||
endpointStates: states,
|
||||
requestHashHeader: b.config.RequestHashHeader,
|
||||
hasEndpointInConnectingState: hasEndpointConnecting,
|
||||
randUint64: rand.Uint64,
|
||||
}
|
||||
}
|
||||
|
||||
// aggregatedStateLocked returns the aggregated child balancers state
|
||||
// based on the following rules.
|
||||
// - If there is at least one endpoint in READY state, report READY.
|
||||
// - If there are 2 or more endpoints in TRANSIENT_FAILURE state, report
|
||||
// TRANSIENT_FAILURE.
|
||||
// - If there is at least one endpoint in CONNECTING state, report CONNECTING.
|
||||
// - If there is one endpoint in TRANSIENT_FAILURE and there is more than one
|
||||
// endpoint, report state CONNECTING.
|
||||
// - If there is at least one endpoint in Idle state, report Idle.
|
||||
// - Otherwise, report TRANSIENT_FAILURE.
|
||||
//
|
||||
// Note that if there are 1 connecting, 2 transient failure, the overall state
|
||||
// is transient failure. This is because the second transient failure is a
|
||||
// fallback of the first failing endpoint, and we want to report transient
|
||||
// failure to failover to the lower priority.
|
||||
func (b *ringhashBalancer) aggregatedStateLocked() connectivity.State {
|
||||
var nums [5]int
|
||||
for _, es := range b.endpointStates.All() {
|
||||
nums[es.state.ConnectivityState]++
|
||||
}
|
||||
|
||||
if nums[connectivity.Ready] > 0 {
|
||||
return connectivity.Ready
|
||||
}
|
||||
if nums[connectivity.TransientFailure] > 1 {
|
||||
return connectivity.TransientFailure
|
||||
}
|
||||
if nums[connectivity.Connecting] > 0 {
|
||||
return connectivity.Connecting
|
||||
}
|
||||
if nums[connectivity.TransientFailure] == 1 && b.endpointStates.Len() > 1 {
|
||||
return connectivity.Connecting
|
||||
}
|
||||
if nums[connectivity.Idle] > 0 {
|
||||
return connectivity.Idle
|
||||
}
|
||||
return connectivity.TransientFailure
|
||||
}
|
||||
|
||||
// getWeightAttribute is a convenience function which returns the value of the
|
||||
// weight endpoint Attribute.
|
||||
//
|
||||
// When used in the xDS context, the weight attribute is guaranteed to be
|
||||
// non-zero. But, when used in a non-xDS context, the weight attribute could be
|
||||
// unset. A Default of 1 is used in the latter case.
|
||||
func getWeightAttribute(e resolver.Endpoint) uint32 {
|
||||
w := weight.FromEndpoint(e).Weight
|
||||
if w == 0 {
|
||||
return 1
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
type endpointState struct {
|
||||
// hashKey is the hash key of the endpoint. Per gRFC A61, each entry in the
|
||||
// ring is an endpoint, positioned based on the hash of the endpoint's first
|
||||
// address by default. Per gRFC A76, the hash key of an endpoint may be
|
||||
// overridden, for example based on EDS endpoint metadata.
|
||||
hashKey string
|
||||
weight uint32
|
||||
exitIdle func()
|
||||
|
||||
// state is updated by the balancer while receiving resolver updates from
|
||||
// the channel and picker updates from its children. Access to it is guarded
|
||||
// by ringhashBalancer.mu.
|
||||
state balancer.State
|
||||
}
|
||||
2944
balancer/ringhash/ringhash_e2e_test.go
Normal file
2944
balancer/ringhash/ringhash_e2e_test.go
Normal file
File diff suppressed because it is too large
Load Diff
739
balancer/ringhash/ringhash_test.go
Normal file
739
balancer/ringhash/ringhash_test.go
Normal file
@@ -0,0 +1,739 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ringhash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/experimental/balancer/weight"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
iringhash "google.golang.org/grpc/internal/ringhash"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
defaultTestShortTimeout = 10 * time.Millisecond
|
||||
|
||||
testBackendAddrsCount = 12
|
||||
)
|
||||
|
||||
var (
|
||||
testBackendAddrStrs []string
|
||||
testConfig = &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 10}
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < testBackendAddrsCount; i++ {
|
||||
testBackendAddrStrs = append(testBackendAddrStrs, fmt.Sprintf("%d.%d.%d.%d:%d", i, i, i, i, i))
|
||||
}
|
||||
}
|
||||
|
||||
// setupTest creates the balancer, and does an initial sanity check.
|
||||
func setupTest(t *testing.T, endpoints []resolver.Endpoint) (*testutils.BalancerClientConn, balancer.Balancer, balancer.Picker) {
|
||||
t.Helper()
|
||||
cc := testutils.NewBalancerClientConn(t)
|
||||
builder := balancer.Get(Name)
|
||||
b := builder.Build(cc, balancer.BuildOptions{})
|
||||
if b == nil {
|
||||
t.Fatalf("builder.Build(%s) failed and returned nil", Name)
|
||||
}
|
||||
t.Cleanup(func() { b.Close() })
|
||||
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: endpoints},
|
||||
BalancerConfig: testConfig,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
|
||||
// The leaf pickfirst are created lazily, only when their endpoint is picked
|
||||
// or other endpoints are in TF. No SubConns should be created immediately.
|
||||
select {
|
||||
case sc := <-cc.NewSubConnCh:
|
||||
t.Errorf("unexpected SubConn creation: %v", sc)
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Should also have a picker, with all endpoints in Idle.
|
||||
p1 := <-cc.NewPickerCh
|
||||
|
||||
ringHashPicker := p1.(*picker)
|
||||
if got, want := len(ringHashPicker.endpointStates), len(endpoints); got != want {
|
||||
t.Errorf("Number of child balancers = %d, want = %d", got, want)
|
||||
}
|
||||
for firstAddr, bs := range ringHashPicker.endpointStates {
|
||||
if got, want := bs.state.ConnectivityState, connectivity.Idle; got != want {
|
||||
t.Errorf("Child balancer connectivity state for address %q = %v, want = %v", firstAddr, got, want)
|
||||
}
|
||||
}
|
||||
return cc, b, p1
|
||||
}
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
// TestUpdateClientConnState_NewRingSize tests the scenario where the ringhash
|
||||
// LB policy receives new configuration which specifies new values for the ring
|
||||
// min and max sizes. The test verifies that a new ring is created and a new
|
||||
// picker is sent to the ClientConn.
|
||||
func (s) TestUpdateClientConnState_NewRingSize(t *testing.T) {
|
||||
origMinRingSize, origMaxRingSize := 1, 10 // Configured from `testConfig` in `setupTest`
|
||||
newMinRingSize, newMaxRingSize := 20, 100
|
||||
|
||||
endpoints := []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}}}
|
||||
cc, b, p1 := setupTest(t, endpoints)
|
||||
ring1 := p1.(*picker).ring
|
||||
if ringSize := len(ring1.items); ringSize < origMinRingSize || ringSize > origMaxRingSize {
|
||||
t.Fatalf("Ring created with size %d, want between [%d, %d]", ringSize, origMinRingSize, origMaxRingSize)
|
||||
}
|
||||
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: endpoints},
|
||||
BalancerConfig: &iringhash.LBConfig{
|
||||
MinRingSize: uint64(newMinRingSize),
|
||||
MaxRingSize: uint64(newMaxRingSize),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
|
||||
var ring2 *ring
|
||||
select {
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Fatal("Timeout when waiting for a picker update after a configuration update")
|
||||
case p2 := <-cc.NewPickerCh:
|
||||
ring2 = p2.(*picker).ring
|
||||
}
|
||||
if ringSize := len(ring2.items); ringSize < newMinRingSize || ringSize > newMaxRingSize {
|
||||
t.Fatalf("Ring created with size %d, want between [%d, %d]", ringSize, newMinRingSize, newMaxRingSize)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestOneEndpoint(t *testing.T) {
|
||||
wantAddr1 := resolver.Address{Addr: testBackendAddrStrs[0]}
|
||||
cc, _, p0 := setupTest(t, []resolver.Endpoint{{Addresses: []resolver.Address{wantAddr1}}})
|
||||
ring0 := p0.(*picker).ring
|
||||
|
||||
firstHash := ring0.items[0].hash
|
||||
// firstHash-1 will pick the first (and only) SubConn from the ring.
|
||||
testHash := firstHash - 1
|
||||
// The first pick should be queued, and should trigger a connection to the
|
||||
// only Endpoint which has a single address.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
|
||||
t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
|
||||
}
|
||||
var sc0 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc0 = <-cc.NewSubConnCh:
|
||||
}
|
||||
if got, want := sc0.Addresses[0].Addr, wantAddr1.Addr; got != want {
|
||||
t.Fatalf("SubConn.Addresses = %v, want = %v", got, want)
|
||||
}
|
||||
select {
|
||||
case <-sc0.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc0)
|
||||
}
|
||||
|
||||
// Send state updates to Ready.
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
if err := cc.WaitForConnectivityState(ctx, connectivity.Ready); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test pick with one backend.
|
||||
p1 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != sc0 {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestThreeBackendsAffinity covers that there are 3 SubConns, RPCs with the
|
||||
// same hash always pick the same SubConn. When the one picked is down, another
|
||||
// one will be picked.
|
||||
func (s) TestThreeSubConnsAffinity(t *testing.T) {
|
||||
endpoints := []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[1]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[2]}}},
|
||||
}
|
||||
remainingAddrs := map[string]bool{
|
||||
testBackendAddrStrs[0]: true,
|
||||
testBackendAddrStrs[1]: true,
|
||||
testBackendAddrStrs[2]: true,
|
||||
}
|
||||
cc, _, p0 := setupTest(t, endpoints)
|
||||
// This test doesn't update addresses, so this ring will be used by all the
|
||||
// pickers.
|
||||
ring := p0.(*picker).ring
|
||||
|
||||
firstHash := ring.items[0].hash
|
||||
// firstHash+1 will pick the second endpoint from the ring.
|
||||
testHash := firstHash + 1
|
||||
// The first pick should be queued, and should trigger Connect() on the only
|
||||
// SubConn.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
|
||||
t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
|
||||
}
|
||||
|
||||
// The picked endpoint should be the second in the ring.
|
||||
var subConns [3]*testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case subConns[1] = <-cc.NewSubConnCh:
|
||||
}
|
||||
if got, want := subConns[1].Addresses[0].Addr, ring.items[1].hashKey; got != want {
|
||||
t.Fatalf("SubConn.Address = %v, want = %v", got, want)
|
||||
}
|
||||
select {
|
||||
case <-subConns[1].ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", subConns[1])
|
||||
}
|
||||
delete(remainingAddrs, ring.items[1].hashKey)
|
||||
|
||||
// Turn down the subConn in use.
|
||||
subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
|
||||
// This should trigger a connection to a new endpoint.
|
||||
<-cc.NewPickerCh
|
||||
var sc *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc = <-cc.NewSubConnCh:
|
||||
}
|
||||
scAddr := sc.Addresses[0].Addr
|
||||
if _, ok := remainingAddrs[scAddr]; !ok {
|
||||
t.Fatalf("New SubConn created with previously used address: %q", scAddr)
|
||||
}
|
||||
delete(remainingAddrs, scAddr)
|
||||
select {
|
||||
case <-sc.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", subConns[1])
|
||||
}
|
||||
if scAddr == ring.items[0].hashKey {
|
||||
subConns[0] = sc
|
||||
} else if scAddr == ring.items[2].hashKey {
|
||||
subConns[2] = sc
|
||||
}
|
||||
|
||||
// Turning down the SubConn should cause creation of a connection to the
|
||||
// final endpoint.
|
||||
sc.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc = <-cc.NewSubConnCh:
|
||||
}
|
||||
scAddr = sc.Addresses[0].Addr
|
||||
if _, ok := remainingAddrs[scAddr]; !ok {
|
||||
t.Fatalf("New SubConn created with previously used address: %q", scAddr)
|
||||
}
|
||||
delete(remainingAddrs, scAddr)
|
||||
select {
|
||||
case <-sc.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", subConns[1])
|
||||
}
|
||||
if scAddr == ring.items[0].hashKey {
|
||||
subConns[0] = sc
|
||||
} else if scAddr == ring.items[2].hashKey {
|
||||
subConns[2] = sc
|
||||
}
|
||||
sc.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
|
||||
// All endpoints are in TransientFailure. Make the first endpoint in the
|
||||
// ring report Ready. All picks should go to this endpoint which is two
|
||||
// indexes away from the endpoint with the chosen hash.
|
||||
subConns[0].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Idle})
|
||||
subConns[0].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
subConns[0].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
if err := cc.WaitForConnectivityState(ctx, connectivity.Ready); err != nil {
|
||||
t.Fatalf("Context timed out while waiting for channel to report Ready.")
|
||||
}
|
||||
p1 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != subConns[0] {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Make the last endpoint in the ring report Ready. All picks should go to
|
||||
// this endpoint since it is one index away from the chosen hash.
|
||||
subConns[2].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Idle})
|
||||
subConns[2].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
subConns[2].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
p2 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != subConns[2] {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Make the second endpoint in the ring report Ready. All picks should go to
|
||||
// this endpoint as it is the one with the chosen hash.
|
||||
subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Idle})
|
||||
subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
p3 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p3.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != subConns[1] {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestThreeBackendsAffinity covers that there are 3 SubConns, RPCs with the
|
||||
// same hash always pick the same SubConn. Then try different hash to pick
|
||||
// another backend, and verify the first hash still picks the first backend.
|
||||
func (s) TestThreeBackendsAffinityMultiple(t *testing.T) {
|
||||
wantEndpoints := []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[1]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[2]}}},
|
||||
}
|
||||
cc, _, p0 := setupTest(t, wantEndpoints)
|
||||
// This test doesn't update addresses, so this ring will be used by all the
|
||||
// pickers.
|
||||
ring0 := p0.(*picker).ring
|
||||
|
||||
firstHash := ring0.items[0].hash
|
||||
// firstHash+1 will pick the second SubConn from the ring.
|
||||
testHash := firstHash + 1
|
||||
// The first pick should be queued, and should trigger Connect() on the only
|
||||
// SubConn.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
|
||||
t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
|
||||
}
|
||||
// The picked SubConn should be the second in the ring.
|
||||
var sc0 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc0 = <-cc.NewSubConnCh:
|
||||
}
|
||||
if got, want := sc0.Addresses[0].Addr, ring0.items[1].hashKey; got != want {
|
||||
t.Fatalf("SubConn.Address = %v, want = %v", got, want)
|
||||
}
|
||||
select {
|
||||
case <-sc0.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc0)
|
||||
}
|
||||
|
||||
// Send state updates to Ready.
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
if err := cc.WaitForConnectivityState(ctx, connectivity.Ready); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First hash should always pick sc0.
|
||||
p1 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != sc0 {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
|
||||
}
|
||||
}
|
||||
|
||||
secondHash := ring0.items[1].hash
|
||||
// secondHash+1 will pick the third SubConn from the ring.
|
||||
testHash2 := secondHash + 1
|
||||
if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash2)}); err != balancer.ErrNoSubConnAvailable {
|
||||
t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
|
||||
}
|
||||
var sc1 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc1 = <-cc.NewSubConnCh:
|
||||
}
|
||||
if got, want := sc1.Addresses[0].Addr, ring0.items[2].hashKey; got != want {
|
||||
t.Fatalf("SubConn.Address = %v, want = %v", got, want)
|
||||
}
|
||||
select {
|
||||
case <-sc1.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc1)
|
||||
}
|
||||
sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
|
||||
// With the new generated picker, hash2 always picks sc1.
|
||||
p2 := <-cc.NewPickerCh
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash2)})
|
||||
if gotSCSt.SubConn != sc1 {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc1)
|
||||
}
|
||||
}
|
||||
// But the first hash still picks sc0.
|
||||
for i := 0; i < 5; i++ {
|
||||
gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
|
||||
if gotSCSt.SubConn != sc0 {
|
||||
t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddrWeightChange covers the following scenarios after setting up the
|
||||
// balancer with 3 addresses [A, B, C]:
|
||||
// - updates balancer with [A, B, C], a new Picker should not be sent.
|
||||
// - updates balancer with [A, B] (C removed), a new Picker is sent and the
|
||||
// ring is updated.
|
||||
// - updates balancer with [A, B], but B has a weight of 2, a new Picker is
|
||||
// sent. And the new ring should contain the correct number of entries
|
||||
// and weights.
|
||||
func (s) TestAddrWeightChange(t *testing.T) {
|
||||
endpoints := []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[1]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[2]}}},
|
||||
}
|
||||
cc, b, p0 := setupTest(t, endpoints)
|
||||
ring0 := p0.(*picker).ring
|
||||
|
||||
// Update with the same addresses, it will result in a new picker, but with
|
||||
// the same ring.
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: endpoints},
|
||||
BalancerConfig: testConfig,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
var p1 balancer.Picker
|
||||
select {
|
||||
case p1 = <-cc.NewPickerCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Fatalf("timeout waiting for picker after UpdateClientConn with same addresses")
|
||||
}
|
||||
ring1 := p1.(*picker).ring
|
||||
if ring1 != ring0 {
|
||||
t.Fatalf("new picker with same address has a different ring than before, want same")
|
||||
}
|
||||
|
||||
// Delete an address, should send a new Picker.
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: endpoints[:2]},
|
||||
BalancerConfig: testConfig,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
var p2 balancer.Picker
|
||||
select {
|
||||
case p2 = <-cc.NewPickerCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Fatalf("timeout waiting for picker after UpdateClientConn with different addresses")
|
||||
}
|
||||
ring2 := p2.(*picker).ring
|
||||
if ring2 == ring0 {
|
||||
t.Fatalf("new picker after removing address has the same ring as before, want different")
|
||||
}
|
||||
|
||||
// Another update with the same addresses, but different weight.
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: []resolver.Endpoint{
|
||||
endpoints[0],
|
||||
weight.Set(endpoints[1], weight.EndpointInfo{Weight: 2}),
|
||||
}},
|
||||
BalancerConfig: testConfig,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
var p3 balancer.Picker
|
||||
select {
|
||||
case p3 = <-cc.NewPickerCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Fatalf("timeout waiting for picker after UpdateClientConn with different addresses")
|
||||
}
|
||||
if p3.(*picker).ring == ring2 {
|
||||
t.Fatalf("new picker after changing address weight has the same ring as before, want different")
|
||||
}
|
||||
// With the new update, the ring must look like this:
|
||||
// [
|
||||
// {idx:0 endpoint: {addr: testBackendAddrStrs[0], weight: 1}},
|
||||
// {idx:1 endpoint: {addr: testBackendAddrStrs[1], weight: 2}},
|
||||
// {idx:2 endpoint: {addr: testBackendAddrStrs[2], weight: 1}},
|
||||
// ].
|
||||
if len(p3.(*picker).ring.items) != 3 {
|
||||
t.Fatalf("new picker after changing address weight has %d entries, want 3", len(p3.(*picker).ring.items))
|
||||
}
|
||||
for _, i := range p3.(*picker).ring.items {
|
||||
if i.hashKey == testBackendAddrStrs[0] {
|
||||
if i.weight != 1 {
|
||||
t.Fatalf("new picker after changing address weight has weight %d for %v, want 1", i.weight, i.hashKey)
|
||||
}
|
||||
}
|
||||
if i.hashKey == testBackendAddrStrs[1] {
|
||||
if i.weight != 2 {
|
||||
t.Fatalf("new picker after changing address weight has weight %d for %v, want 2", i.weight, i.hashKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoConnectEndpointOnTransientFailure covers the situation when an
|
||||
// endpoint fails. It verifies that a new endpoint is automatically tried
|
||||
// (without a pick) when there is no endpoint already in Connecting state.
|
||||
func (s) TestAutoConnectEndpointOnTransientFailure(t *testing.T) {
|
||||
wantEndpoints := []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[1]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[2]}}},
|
||||
{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[3]}}},
|
||||
}
|
||||
cc, _, p0 := setupTest(t, wantEndpoints)
|
||||
|
||||
// ringhash won't tell SCs to connect until there is an RPC, so simulate
|
||||
// one now.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
ctx = iringhash.SetXDSRequestHash(ctx, 0)
|
||||
defer cancel()
|
||||
p0.Pick(balancer.PickInfo{Ctx: ctx})
|
||||
|
||||
// The picked SubConn should be the second in the ring.
|
||||
var sc0 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc0 = <-cc.NewSubConnCh:
|
||||
}
|
||||
select {
|
||||
case <-sc0.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc0)
|
||||
}
|
||||
|
||||
// Turn the first subconn to transient failure. This should set the overall
|
||||
// connectivity state to CONNECTING.
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
cc.WaitForConnectivityState(ctx, connectivity.Connecting)
|
||||
|
||||
// It will trigger the second subconn to connect since there is only one
|
||||
// endpoint, which is in TF.
|
||||
var sc1 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc1 = <-cc.NewSubConnCh:
|
||||
}
|
||||
select {
|
||||
case <-sc1.ConnectCh:
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
t.Fatalf("timeout waiting for Connect() from SubConn %v", sc1)
|
||||
}
|
||||
|
||||
// Turn the second subconn to TF. This will set the overall state to TF.
|
||||
sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
cc.WaitForConnectivityState(ctx, connectivity.TransientFailure)
|
||||
|
||||
// It will trigger the third subconn to connect.
|
||||
var sc2 *testutils.TestSubConn
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case sc2 = <-cc.NewSubConnCh:
|
||||
}
|
||||
select {
|
||||
case <-sc2.ConnectCh:
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
t.Fatalf("timeout waiting for Connect() from SubConn %v", sc2)
|
||||
}
|
||||
|
||||
sc2.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
|
||||
// Send the first SubConn into CONNECTING. To do this, first make it READY,
|
||||
// then CONNECTING.
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
|
||||
cc.WaitForConnectivityState(ctx, connectivity.Ready)
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Idle})
|
||||
// Since one endpoint is in TF and one in CONNECTING, the aggregated state
|
||||
// will be CONNECTING.
|
||||
cc.WaitForConnectivityState(ctx, connectivity.Connecting)
|
||||
p1 := <-cc.NewPickerCh
|
||||
p1.Pick(balancer.PickInfo{Ctx: ctx})
|
||||
select {
|
||||
case <-sc0.ConnectCh:
|
||||
case <-time.After(defaultTestTimeout):
|
||||
t.Errorf("timeout waiting for Connect() from SubConn %v", sc0)
|
||||
}
|
||||
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
|
||||
|
||||
// This will not trigger any new SubCOnns to be created, because sc0 is
|
||||
// still attempting to connect, and we only need one SubConn to connect.
|
||||
sc2.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
|
||||
|
||||
select {
|
||||
case sc := <-cc.NewSubConnCh:
|
||||
t.Fatalf("unexpected SubConn creation: %v", sc)
|
||||
case <-sc0.ConnectCh:
|
||||
t.Fatalf("unexpected Connect() from SubConn %v", sc0)
|
||||
case <-sc1.ConnectCh:
|
||||
t.Fatalf("unexpected Connect() from SubConn %v", sc1)
|
||||
case <-sc2.ConnectCh:
|
||||
t.Fatalf("unexpected Connect() from SubConn %v", sc2)
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestAggregatedConnectivityState(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpointStates []connectivity.State
|
||||
want connectivity.State
|
||||
}{
|
||||
{
|
||||
name: "one ready",
|
||||
endpointStates: []connectivity.State{connectivity.Ready},
|
||||
want: connectivity.Ready,
|
||||
},
|
||||
{
|
||||
name: "one connecting",
|
||||
endpointStates: []connectivity.State{connectivity.Connecting},
|
||||
want: connectivity.Connecting,
|
||||
},
|
||||
{
|
||||
name: "one ready one transient failure",
|
||||
endpointStates: []connectivity.State{connectivity.Ready, connectivity.TransientFailure},
|
||||
want: connectivity.Ready,
|
||||
},
|
||||
{
|
||||
name: "one connecting one transient failure",
|
||||
endpointStates: []connectivity.State{connectivity.Connecting, connectivity.TransientFailure},
|
||||
want: connectivity.Connecting,
|
||||
},
|
||||
{
|
||||
name: "one connecting two transient failure",
|
||||
endpointStates: []connectivity.State{connectivity.Connecting, connectivity.TransientFailure, connectivity.TransientFailure},
|
||||
want: connectivity.TransientFailure,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bal := &ringhashBalancer{endpointStates: resolver.NewEndpointMap[*endpointState]()}
|
||||
for i, cs := range tt.endpointStates {
|
||||
es := &endpointState{
|
||||
state: balancer.State{ConnectivityState: cs},
|
||||
}
|
||||
ep := resolver.Endpoint{Addresses: []resolver.Address{{Addr: fmt.Sprintf("%d.%d.%d.%d:%d", i, i, i, i, i)}}}
|
||||
bal.endpointStates.Set(ep, es)
|
||||
}
|
||||
if got := bal.aggregatedStateLocked(); got != tt.want {
|
||||
t.Errorf("recordTransition() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testKeyType string
|
||||
|
||||
const testKey testKeyType = "grpc.lb.ringhash.testKey"
|
||||
|
||||
type testAttribute struct {
|
||||
content string
|
||||
}
|
||||
|
||||
func setTestAttrAddr(addr resolver.Address, content string) resolver.Address {
|
||||
addr.BalancerAttributes = addr.BalancerAttributes.WithValue(testKey, testAttribute{content})
|
||||
return addr
|
||||
}
|
||||
|
||||
func setTestAttrEndpoint(endpoint resolver.Endpoint, content string) resolver.Endpoint {
|
||||
endpoint.Attributes = endpoint.Attributes.WithValue(testKey, testAttribute{content})
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// TestAddrBalancerAttributesChange tests the case where the ringhash balancer
|
||||
// receives a ClientConnUpdate with the same config and addresses as received in
|
||||
// the previous update. Although the `BalancerAttributes` and endpoint
|
||||
// attributes contents are the same, the pointers are different. This test
|
||||
// verifies that subConns are not recreated in this scenario.
|
||||
func (s) TestAddrBalancerAttributesChange(t *testing.T) {
|
||||
content := "test"
|
||||
addrs1 := []resolver.Address{setTestAttrAddr(resolver.Address{Addr: testBackendAddrStrs[0]}, content)}
|
||||
wantEndpoints1 := []resolver.Endpoint{
|
||||
setTestAttrEndpoint(resolver.Endpoint{Addresses: addrs1}, "content"),
|
||||
}
|
||||
cc, b, p0 := setupTest(t, wantEndpoints1)
|
||||
ring0 := p0.(*picker).ring
|
||||
|
||||
firstHash := ring0.items[0].hash
|
||||
// The first pick should be queued, and should trigger a connection to the
|
||||
// only Endpoint which has a single address.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, firstHash)}); err != balancer.ErrNoSubConnAvailable {
|
||||
t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for SubConn creation.")
|
||||
case <-cc.NewSubConnCh:
|
||||
}
|
||||
|
||||
addrs2 := []resolver.Address{setTestAttrAddr(resolver.Address{Addr: testBackendAddrStrs[0]}, content)}
|
||||
wantEndpoints2 := []resolver.Endpoint{setTestAttrEndpoint(resolver.Endpoint{Addresses: addrs2}, content)}
|
||||
if err := b.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Endpoints: wantEndpoints2},
|
||||
BalancerConfig: testConfig,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateClientConnState returned err: %v", err)
|
||||
}
|
||||
select {
|
||||
case <-cc.NewSubConnCh:
|
||||
t.Fatal("new subConn created for an update with the same addresses")
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user