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:
385
balancer/endpointsharding/endpointsharding.go
Normal file
385
balancer/endpointsharding/endpointsharding.go
Normal file
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 endpointsharding implements a load balancing policy that manages
|
||||
// homogeneous child policies each owning a single endpoint.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This package is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
package endpointsharding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
rand "math/rand/v2"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/base"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
var randIntN = rand.IntN
|
||||
|
||||
// ChildState is the state of a child balancer.
|
||||
type ChildState struct {
|
||||
Endpoint resolver.Endpoint // Endpoint of the child balancer.
|
||||
State balancer.State // State of the child balancer.
|
||||
ExitIdle func() // Function to exit the child balancer from IDLE state.
|
||||
}
|
||||
|
||||
// Options configure the behaviour of the endpointsharding balancer.
|
||||
type Options struct {
|
||||
// DisableAutoReconnect allows the balancer to keep child balancer in the
|
||||
// IDLE state until they are explicitly triggered to exit using the
|
||||
// ChildState obtained from the endpointsharding picker. When set to false,
|
||||
// the endpointsharding balancer will automatically call ExitIdle on child
|
||||
// connections that report IDLE.
|
||||
DisableAutoReconnect bool
|
||||
}
|
||||
|
||||
// ChildBuilderFunc creates a new balancer with the ClientConn. It has the same
|
||||
// type as the balancer.Builder.Build method.
|
||||
type ChildBuilderFunc func(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer
|
||||
|
||||
// NewBalancer returns a load balancing policy that manages homogeneous child
|
||||
// policies each owning a single endpoint. The endpointsharding balancer
|
||||
// forwards the LoadBalancingConfig in ClientConn state updates to its children.
|
||||
func NewBalancer(cc balancer.ClientConn, opts balancer.BuildOptions, childBuilder ChildBuilderFunc, esOpts Options) balancer.Balancer {
|
||||
return &endpointSharding{
|
||||
cc: cc,
|
||||
bOpts: opts,
|
||||
esOpts: esOpts,
|
||||
childBuilder: childBuilder,
|
||||
endpoints: resolver.NewEndpointMap[*endpointState](),
|
||||
}
|
||||
}
|
||||
|
||||
// endpointSharding is a balancer that wraps child balancers. It creates a child
|
||||
// balancer with child config for every unique Endpoint received. It updates the
|
||||
// child states on any update from parent or child.
|
||||
type endpointSharding struct {
|
||||
cc balancer.ClientConn
|
||||
bOpts balancer.BuildOptions
|
||||
esOpts Options
|
||||
childBuilder ChildBuilderFunc
|
||||
|
||||
// mu guards access to the below fields and guarantees mutual exclusion
|
||||
// between top-down methods (like UpdateClientConnState, ResolverError etc,
|
||||
// which are already serialized) and bottom-up methods (like UpdateState)
|
||||
// that can be called concurrently.
|
||||
//
|
||||
// Lock ordering & deadlock prevention:
|
||||
// Child callbacks (like UpdateState) acquire this mutex to push state updates
|
||||
// to the parent. This establishes a strict lock ordering:
|
||||
// [endpointState.childMu] -> [endpointSharding.mu]
|
||||
//
|
||||
// To prevent deadlocks, we must never invert this order. Therefore, we must
|
||||
// never call any methods on a child balancer while holding this mutex. If we
|
||||
// did, and that child balancer invoked UpdateState synchronously, it would
|
||||
// attempt to re-acquire this mutex, causing a deadlock.
|
||||
//
|
||||
// Concurrency:
|
||||
// Top-down operations need to iterate over all children. To ensure a
|
||||
// single, clean aggregated update at the end of such operations, we inhibit
|
||||
// intermediate updates from children. We grab this mutex briefly to set
|
||||
// `inhibitChildUpdates = true` and immediately release it. This allows us
|
||||
// to perform the potentially slow, top-down child updates without holding
|
||||
// any parent locks. Once finished, we grab the mutex again to unset the
|
||||
// flag and push the final aggregated state. An update from a child during
|
||||
// this time will *only* update the child state, and will not access the
|
||||
// endpoints map or push an aggregated state to the parent.
|
||||
mu sync.Mutex
|
||||
endpoints *resolver.EndpointMap[*endpointState]
|
||||
inhibitChildUpdates bool
|
||||
}
|
||||
|
||||
// rotateEndpoints returns a slice of all the input endpoints rotated a random
|
||||
// amount.
|
||||
func rotateEndpoints(es []resolver.Endpoint) []resolver.Endpoint {
|
||||
n := len(es)
|
||||
if n == 0 {
|
||||
return es
|
||||
}
|
||||
r := randIntN(n)
|
||||
|
||||
// Make a copy to avoid mutating data beyond the end of es.
|
||||
ret := make([]resolver.Endpoint, n)
|
||||
copy(ret, es[r:])
|
||||
copy(ret[n-r:], es[:r])
|
||||
return ret
|
||||
}
|
||||
|
||||
// UpdateClientConnState creates a child for new endpoints and deletes children
|
||||
// for endpoints that are no longer present. It also updates all the children,
|
||||
// and sends a single synchronous update of the childrens' aggregated state at
|
||||
// the end of the UpdateClientConnState operation.
|
||||
//
|
||||
// Returns the first error found from a child, but fully processes the update.
|
||||
func (es *endpointSharding) UpdateClientConnState(state balancer.ClientConnState) error {
|
||||
es.inhibitUpdatesFromChildren()
|
||||
|
||||
// Update/create child balancers for each endpoint in the update. Note that we
|
||||
// don't hold the mutex here, but this is fine because inhibitChildUpdates is
|
||||
// true, and therefore UpdateState will not access es.endpoints.
|
||||
var retErr error
|
||||
newEndpoints := resolver.NewEndpointMap[*endpointState]()
|
||||
for _, endpoint := range rotateEndpoints(state.ResolverState.Endpoints) {
|
||||
if _, ok := newEndpoints.Get(endpoint); ok {
|
||||
// Skip duplicate endpoints.
|
||||
continue
|
||||
}
|
||||
epState, ok := es.endpoints.Get(endpoint)
|
||||
if ok {
|
||||
// Endpoint child already exists, update the stored endpoint.
|
||||
epState.endpoint = endpoint
|
||||
} else {
|
||||
// Endpoint child does not exist, create a new one.
|
||||
epState = &endpointState{
|
||||
ClientConn: es.cc,
|
||||
parent: es,
|
||||
endpoint: endpoint,
|
||||
disableAutoReconnect: es.esOpts.DisableAutoReconnect,
|
||||
}
|
||||
epState.childLB = es.childBuilder(epState, es.bOpts)
|
||||
}
|
||||
// Update the endpoint state for the endpoint.
|
||||
newEndpoints.Set(endpoint, epState)
|
||||
|
||||
if err := epState.updateClientConnState(balancer.ClientConnState{
|
||||
BalancerConfig: state.BalancerConfig,
|
||||
ResolverState: resolver.State{
|
||||
Endpoints: []resolver.Endpoint{endpoint},
|
||||
Attributes: state.ResolverState.Attributes,
|
||||
},
|
||||
}); err != nil && retErr == nil {
|
||||
// Keep the first error found from any child.
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old children that are no longer present.
|
||||
for e, child := range es.endpoints.All() {
|
||||
if _, ok := newEndpoints.Get(e); !ok {
|
||||
child.close()
|
||||
}
|
||||
}
|
||||
|
||||
if newEndpoints.Len() == 0 {
|
||||
retErr = balancer.ErrBadResolverState
|
||||
}
|
||||
|
||||
es.mu.Lock()
|
||||
es.endpoints = newEndpoints
|
||||
es.inhibitChildUpdates = false
|
||||
es.updateStateLocked()
|
||||
es.mu.Unlock()
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// ResolverError forwards the resolver error to all of the endpointSharding's
|
||||
// children and sends a single synchronous update of the childStates at the end
|
||||
// of the ResolverError operation.
|
||||
func (es *endpointSharding) ResolverError(err error) {
|
||||
es.inhibitUpdatesFromChildren()
|
||||
for _, child := range es.endpoints.All() {
|
||||
child.resolverError(err)
|
||||
}
|
||||
es.allowUpdatesFromChildren()
|
||||
}
|
||||
|
||||
func (es *endpointSharding) UpdateSubConnState(balancer.SubConn, balancer.SubConnState) {
|
||||
// UpdateSubConnState is deprecated.
|
||||
}
|
||||
|
||||
func (es *endpointSharding) Close() {
|
||||
es.inhibitUpdatesFromChildren()
|
||||
for _, child := range es.endpoints.All() {
|
||||
child.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (es *endpointSharding) ExitIdle() {
|
||||
es.inhibitUpdatesFromChildren()
|
||||
for _, child := range es.endpoints.All() {
|
||||
child.exitIdle()
|
||||
}
|
||||
es.allowUpdatesFromChildren()
|
||||
}
|
||||
|
||||
func (es *endpointSharding) inhibitUpdatesFromChildren() {
|
||||
es.mu.Lock()
|
||||
es.inhibitChildUpdates = true
|
||||
es.mu.Unlock()
|
||||
}
|
||||
|
||||
func (es *endpointSharding) allowUpdatesFromChildren() {
|
||||
es.mu.Lock()
|
||||
es.inhibitChildUpdates = false
|
||||
es.updateStateLocked()
|
||||
es.mu.Unlock()
|
||||
}
|
||||
|
||||
// updateStateLocked updates this component's state. It sends the aggregated
|
||||
// state, and a picker with round robin behavior with all the child states
|
||||
// present if needed. This method must only be called when inhibitChildUpdates
|
||||
// is false.
|
||||
//
|
||||
// Caller must hold es.mu.
|
||||
func (es *endpointSharding) updateStateLocked() {
|
||||
var readyPickers, connectingPickers, idlePickers, transientFailurePickers []balancer.Picker
|
||||
|
||||
childStates := make([]ChildState, 0, es.endpoints.Len())
|
||||
for _, epState := range es.endpoints.All() {
|
||||
childState := ChildState{
|
||||
Endpoint: epState.endpoint,
|
||||
State: epState.state,
|
||||
ExitIdle: func() { go epState.exitIdle() },
|
||||
}
|
||||
childStates = append(childStates, childState)
|
||||
childPicker := childState.State.Picker
|
||||
switch childState.State.ConnectivityState {
|
||||
case connectivity.Ready:
|
||||
readyPickers = append(readyPickers, childPicker)
|
||||
case connectivity.Connecting:
|
||||
connectingPickers = append(connectingPickers, childPicker)
|
||||
case connectivity.Idle:
|
||||
idlePickers = append(idlePickers, childPicker)
|
||||
case connectivity.TransientFailure:
|
||||
transientFailurePickers = append(transientFailurePickers, childPicker)
|
||||
// connectivity.Shutdown shouldn't appear.
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the round robin picker based off the aggregated state. Whatever
|
||||
// the aggregated state, use the pickers present that are currently in that
|
||||
// state only.
|
||||
var aggState connectivity.State
|
||||
var pickers []balancer.Picker
|
||||
if len(readyPickers) >= 1 {
|
||||
aggState = connectivity.Ready
|
||||
pickers = readyPickers
|
||||
} else if len(connectingPickers) >= 1 {
|
||||
aggState = connectivity.Connecting
|
||||
pickers = connectingPickers
|
||||
} else if len(idlePickers) >= 1 {
|
||||
aggState = connectivity.Idle
|
||||
pickers = idlePickers
|
||||
} else if len(transientFailurePickers) >= 1 {
|
||||
aggState = connectivity.TransientFailure
|
||||
pickers = transientFailurePickers
|
||||
} else {
|
||||
aggState = connectivity.TransientFailure
|
||||
pickers = []balancer.Picker{base.NewErrPicker(errors.New("no children to pick from"))}
|
||||
} // No children (resolver error before valid update).
|
||||
|
||||
es.cc.UpdateState(balancer.State{
|
||||
ConnectivityState: aggState,
|
||||
Picker: &pickerWithChildStates{
|
||||
pickers: pickers,
|
||||
childStates: childStates,
|
||||
next: uint32(randIntN(len(pickers))),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// pickerWithChildStates delegates to the pickers it holds in a round robin
|
||||
// fashion. It also contains the childStates of all the endpointSharding's
|
||||
// children.
|
||||
type pickerWithChildStates struct {
|
||||
pickers []balancer.Picker
|
||||
childStates []ChildState
|
||||
next uint32
|
||||
}
|
||||
|
||||
func (p *pickerWithChildStates) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
|
||||
nextIndex := atomic.AddUint32(&p.next, 1)
|
||||
picker := p.pickers[nextIndex%uint32(len(p.pickers))]
|
||||
return picker.Pick(info)
|
||||
}
|
||||
|
||||
// ChildStatesFromPicker returns the state of all the children managed by the
|
||||
// endpoint sharding balancer that created this picker.
|
||||
func ChildStatesFromPicker(picker balancer.Picker) []ChildState {
|
||||
p, ok := picker.(*pickerWithChildStates)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p.childStates
|
||||
}
|
||||
|
||||
// endpointState is the internal state maintained for each endpoint.
|
||||
type endpointState struct {
|
||||
balancer.ClientConn // Embedded to intercept UpdateState
|
||||
|
||||
parent *endpointSharding // Parent endpointsharding balancer.
|
||||
endpoint resolver.Endpoint // Endpoint of the child balancer.
|
||||
state balancer.State // State of the child balancer.
|
||||
disableAutoReconnect bool // Whether to disable auto reconnect for this child.
|
||||
|
||||
childMu sync.Mutex // Guarantees mutual exclusion for Balancer API calls to the child balancer.
|
||||
childLB balancer.Balancer // Child balancer.
|
||||
closed bool // Tracks closure of the child balancer to ensure ExitIdle is not called after Close().
|
||||
}
|
||||
|
||||
func (es *endpointState) UpdateState(state balancer.State) {
|
||||
es.parent.mu.Lock()
|
||||
es.state = state
|
||||
if !es.parent.inhibitChildUpdates {
|
||||
es.parent.updateStateLocked()
|
||||
}
|
||||
es.parent.mu.Unlock()
|
||||
|
||||
if state.ConnectivityState == connectivity.Idle && !es.disableAutoReconnect {
|
||||
go es.exitIdle()
|
||||
}
|
||||
}
|
||||
|
||||
func (es *endpointState) updateClientConnState(state balancer.ClientConnState) error {
|
||||
es.childMu.Lock()
|
||||
err := es.childLB.UpdateClientConnState(state)
|
||||
es.childMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (es *endpointState) resolverError(err error) {
|
||||
es.childMu.Lock()
|
||||
es.childLB.ResolverError(err)
|
||||
es.childMu.Unlock()
|
||||
}
|
||||
|
||||
func (es *endpointState) close() {
|
||||
es.childMu.Lock()
|
||||
if !es.closed {
|
||||
es.closed = true
|
||||
es.childLB.Close()
|
||||
}
|
||||
es.childMu.Unlock()
|
||||
}
|
||||
|
||||
func (es *endpointState) exitIdle() {
|
||||
es.childMu.Lock()
|
||||
if !es.closed {
|
||||
es.childLB.ExitIdle()
|
||||
}
|
||||
es.childMu.Unlock()
|
||||
}
|
||||
353
balancer/endpointsharding/endpointsharding_ext_test.go
Normal file
353
balancer/endpointsharding/endpointsharding_ext_test.go
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 endpointsharding_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/endpointsharding"
|
||||
"google.golang.org/grpc/balancer/pickfirst"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/balancer/stub"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/roundrobin"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTestTimeout = time.Second * 10
|
||||
defaultTestShortTimeout = time.Millisecond * 10
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
var logger = grpclog.Component("endpoint-sharding-test")
|
||||
|
||||
func init() {
|
||||
balancer.Register(fakePetioleBuilder{})
|
||||
}
|
||||
|
||||
const fakePetioleName = "fake_petiole"
|
||||
|
||||
type fakePetioleBuilder struct{}
|
||||
|
||||
func (fakePetioleBuilder) Name() string {
|
||||
return fakePetioleName
|
||||
}
|
||||
|
||||
func (fakePetioleBuilder) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
fp := &fakePetiole{
|
||||
ClientConn: cc,
|
||||
bOpts: opts,
|
||||
}
|
||||
fp.Balancer = endpointsharding.NewBalancer(fp, opts, balancer.Get(pickfirst.Name).Build, endpointsharding.Options{})
|
||||
return fp
|
||||
}
|
||||
|
||||
func (fakePetioleBuilder) ParseConfig(json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// fakePetiole is a load balancer that wraps the endpointShardingBalancer, and
|
||||
// forwards ClientConnUpdates with a child config of graceful switch that wraps
|
||||
// pick first. It also intercepts UpdateState to make sure it can access the
|
||||
// child state maintained by EndpointSharding.
|
||||
type fakePetiole struct {
|
||||
balancer.Balancer
|
||||
balancer.ClientConn
|
||||
bOpts balancer.BuildOptions
|
||||
}
|
||||
|
||||
func (fp *fakePetiole) UpdateClientConnState(state balancer.ClientConnState) error {
|
||||
if el := state.ResolverState.Endpoints; len(el) != 2 {
|
||||
return fmt.Errorf("UpdateClientConnState wants two endpoints, got: %v", el)
|
||||
}
|
||||
|
||||
return fp.Balancer.UpdateClientConnState(state)
|
||||
}
|
||||
|
||||
func (fp *fakePetiole) UpdateState(state balancer.State) {
|
||||
childStates := endpointsharding.ChildStatesFromPicker(state.Picker)
|
||||
// Both child states should be present in the child picker. States and
|
||||
// picker change over the lifecycle of test, but there should always be two.
|
||||
if len(childStates) != 2 {
|
||||
logger.Fatal(fmt.Errorf("length of child states received: %v, want 2", len(childStates)))
|
||||
}
|
||||
|
||||
fp.ClientConn.UpdateState(state)
|
||||
}
|
||||
|
||||
// TestEndpointShardingBasic tests the basic functionality of the endpoint
|
||||
// sharding balancer. It specifies a petiole policy that is essentially a
|
||||
// wrapper around the endpoint sharder. Two backends are started, with each
|
||||
// backend's address specified in an endpoint. The petiole does not have a
|
||||
// special picker, so it should fallback to the default behavior, which is to
|
||||
// round_robin amongst the endpoint children that are in the aggregated state.
|
||||
// It also verifies the petiole has access to the raw child state in case it
|
||||
// wants to implement a custom picker. The test sends a resolver error to the
|
||||
// endpointsharding balancer and verifies an error picker from the children
|
||||
// is used while making an RPC.
|
||||
func (s) TestEndpointShardingBasic(t *testing.T) {
|
||||
backend1 := stubserver.StartTestService(t, nil)
|
||||
defer backend1.Stop()
|
||||
backend2 := stubserver.StartTestService(t, nil)
|
||||
defer backend2.Stop()
|
||||
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
defer mr.Close()
|
||||
|
||||
json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, fakePetioleName)
|
||||
sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
|
||||
mr.InitialState(resolver.State{
|
||||
Endpoints: []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: backend1.Address}}},
|
||||
{Addresses: []resolver.Address{{Addr: backend2.Address}}},
|
||||
},
|
||||
ServiceConfig: sc,
|
||||
})
|
||||
|
||||
dOpts := []grpc.DialOption{
|
||||
grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
// Use a large backoff delay to avoid the error picker being updated
|
||||
// too quickly.
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 2 * defaultTestTimeout,
|
||||
Multiplier: float64(0),
|
||||
Jitter: float64(0),
|
||||
MaxDelay: 2 * defaultTestTimeout,
|
||||
},
|
||||
}),
|
||||
}
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", dOpts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
// Assert a round robin distribution between the two spun up backends. This
|
||||
// requires a poll and eventual consistency as both endpoint children do not
|
||||
// start in state READY.
|
||||
if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend1.Address}, {Addr: backend2.Address}}); err != nil {
|
||||
t.Fatalf("error in expected round robin: %v", err)
|
||||
}
|
||||
|
||||
// Stopping both the backends should make the channel enter
|
||||
// TransientFailure.
|
||||
backend1.Stop()
|
||||
backend2.Stop()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
|
||||
// When the resolver reports an error, the picker should get updated to
|
||||
// return the resolver error.
|
||||
mr.CC().ReportError(errors.New("test error"))
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
for ; ctx.Err() == nil; <-time.After(time.Millisecond) {
|
||||
_, err := client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if err == nil {
|
||||
t.Fatalf("EmptyCall succeeded when expected to fail with %q", "test error")
|
||||
}
|
||||
if strings.Contains(err.Error(), "test error") {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("Context timed out waiting for picker with resolver error.")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that endpointsharding doesn't automatically re-connect IDLE children.
|
||||
// The test creates an endpoint with two servers and another with a single
|
||||
// server. The active service in endpoint 1 is closed to make the child
|
||||
// pickfirst enter IDLE state. The test verifies that the child pickfirst
|
||||
// doesn't connect to the second address in the endpoint.
|
||||
func (s) TestEndpointShardingReconnectDisabled(t *testing.T) {
|
||||
backend1 := stubserver.StartTestService(t, nil)
|
||||
defer backend1.Stop()
|
||||
backend2 := stubserver.StartTestService(t, nil)
|
||||
defer backend2.Stop()
|
||||
backend3 := stubserver.StartTestService(t, nil)
|
||||
defer backend3.Stop()
|
||||
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
defer mr.Close()
|
||||
|
||||
name := strings.ReplaceAll(strings.ToLower(t.Name()), "/", "")
|
||||
bf := stub.BalancerFuncs{
|
||||
Init: func(bd *stub.BalancerData) {
|
||||
epOpts := endpointsharding.Options{DisableAutoReconnect: true}
|
||||
bd.ChildBalancer = endpointsharding.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirst.Name).Build, epOpts)
|
||||
},
|
||||
UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
|
||||
return bd.ChildBalancer.UpdateClientConnState(ccs)
|
||||
},
|
||||
Close: func(bd *stub.BalancerData) {
|
||||
bd.ChildBalancer.Close()
|
||||
},
|
||||
}
|
||||
stub.Register(name, bf)
|
||||
|
||||
json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, name)
|
||||
sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
|
||||
mr.InitialState(resolver.State{
|
||||
Endpoints: []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: backend1.Address}, {Addr: backend2.Address}}},
|
||||
{Addresses: []resolver.Address{{Addr: backend3.Address}}},
|
||||
},
|
||||
ServiceConfig: sc,
|
||||
})
|
||||
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
// Assert a round robin distribution between the two spun up backends. This
|
||||
// requires a poll and eventual consistency as both endpoint children do not
|
||||
// start in state READY.
|
||||
if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend1.Address}, {Addr: backend3.Address}}); err != nil {
|
||||
t.Fatalf("error in expected round robin: %v", err)
|
||||
}
|
||||
|
||||
// On closing the first server, the first child balancer should enter
|
||||
// IDLE. Since endpointsharding is configured not to auto-reconnect, it will
|
||||
// remain IDLE and will not try to connect to the second backend in the same
|
||||
// endpoint.
|
||||
backend1.Stop()
|
||||
// CheckRoundRobinRPCs waits for all the backends to become reachable, we
|
||||
// call it to ensure the picker no longer sends RPCs to closed backend.
|
||||
if err = roundrobin.CheckRoundRobinRPCs(ctx, client, []resolver.Address{{Addr: backend3.Address}}); err != nil {
|
||||
t.Fatalf("error in expected round robin: %v", err)
|
||||
}
|
||||
|
||||
// Verify requests go only to backend3 for a short time.
|
||||
shortCtx, cancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer cancel()
|
||||
for ; shortCtx.Err() == nil; <-time.After(time.Millisecond) {
|
||||
var peer peer.Peer
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
|
||||
if status.Code(err) != codes.DeadlineExceeded {
|
||||
t.Fatalf("EmptyCall() returned unexpected error %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if got, want := peer.Addr.String(), backend3.Address; got != want {
|
||||
t.Fatalf("EmptyCall() went to unexpected backend: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that endpointsharding doesn't automatically re-connect IDLE children
|
||||
// until cc.Connect() is called. The test creates an endpoint with a single
|
||||
// address. The client is connected and the active server is closed to make the
|
||||
// child pickfirst enter IDLE state. The test verifies that the child pickfirst
|
||||
// doesn't re-connect automatically. The test calls cc.Connect() and verified
|
||||
// that the balancer connects causing the channel to enter TransientFailure.
|
||||
func (s) TestEndpointShardingExitIdle(t *testing.T) {
|
||||
backend := stubserver.StartTestService(t, nil)
|
||||
defer backend.Stop()
|
||||
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
defer mr.Close()
|
||||
|
||||
name := strings.ReplaceAll(strings.ToLower(t.Name()), "/", "")
|
||||
bf := stub.BalancerFuncs{
|
||||
Init: func(bd *stub.BalancerData) {
|
||||
epOpts := endpointsharding.Options{DisableAutoReconnect: true}
|
||||
bd.ChildBalancer = endpointsharding.NewBalancer(bd.ClientConn, bd.BuildOptions, balancer.Get(pickfirst.Name).Build, epOpts)
|
||||
},
|
||||
UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
|
||||
return bd.ChildBalancer.UpdateClientConnState(ccs)
|
||||
},
|
||||
Close: func(bd *stub.BalancerData) {
|
||||
bd.ChildBalancer.Close()
|
||||
},
|
||||
ExitIdle: func(bd *stub.BalancerData) {
|
||||
bd.ChildBalancer.ExitIdle()
|
||||
},
|
||||
}
|
||||
stub.Register(name, bf)
|
||||
|
||||
json := fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, name)
|
||||
sc := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(json)
|
||||
mr.InitialState(resolver.State{
|
||||
Endpoints: []resolver.Endpoint{
|
||||
{Addresses: []resolver.Address{{Addr: backend.Address}}},
|
||||
},
|
||||
ServiceConfig: sc,
|
||||
})
|
||||
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Errorf("client.EmptyCall() returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// On closing the first server, the first child balancer should enter
|
||||
// IDLE. Since endpointsharding is configured not to auto-reconnect, it will
|
||||
// remain IDLE and will not try to re-connect
|
||||
backend.Stop()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Idle)
|
||||
shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer shortCancel()
|
||||
testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Idle)
|
||||
|
||||
// The balancer should try to re-connect and fail.
|
||||
cc.Connect()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
}
|
||||
83
balancer/endpointsharding/endpointsharding_test.go
Normal file
83
balancer/endpointsharding/endpointsharding_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2025 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 endpointsharding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestRotateEndpoints(t *testing.T) {
|
||||
ep := func(addr string) resolver.Endpoint {
|
||||
return resolver.Endpoint{Addresses: []resolver.Address{{Addr: addr}}}
|
||||
}
|
||||
endpoints := []resolver.Endpoint{ep("1"), ep("2"), ep("3"), ep("4"), ep("5")}
|
||||
testCases := []struct {
|
||||
rval int
|
||||
want []resolver.Endpoint
|
||||
}{
|
||||
{
|
||||
rval: 0,
|
||||
want: []resolver.Endpoint{ep("1"), ep("2"), ep("3"), ep("4"), ep("5")},
|
||||
},
|
||||
{
|
||||
rval: 1,
|
||||
want: []resolver.Endpoint{ep("2"), ep("3"), ep("4"), ep("5"), ep("1")},
|
||||
},
|
||||
{
|
||||
rval: 2,
|
||||
want: []resolver.Endpoint{ep("3"), ep("4"), ep("5"), ep("1"), ep("2")},
|
||||
},
|
||||
{
|
||||
rval: 3,
|
||||
want: []resolver.Endpoint{ep("4"), ep("5"), ep("1"), ep("2"), ep("3")},
|
||||
},
|
||||
{
|
||||
rval: 4,
|
||||
want: []resolver.Endpoint{ep("5"), ep("1"), ep("2"), ep("3"), ep("4")},
|
||||
},
|
||||
}
|
||||
|
||||
defer func(r func(int) int) {
|
||||
randIntN = r
|
||||
}(randIntN)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprint(tc.rval), func(t *testing.T) {
|
||||
randIntN = func(int) int {
|
||||
return tc.rval
|
||||
}
|
||||
got := rotateEndpoints(endpoints)
|
||||
if fmt.Sprint(got) != fmt.Sprint(tc.want) {
|
||||
t.Fatalf("rand=%v; rotateEndpoints(%v) = %v; want %v", tc.rval, endpoints, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user