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:
292
internal/xds/balancer/priority/balancer.go
Normal file
292
internal/xds/balancer/priority/balancer.go
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
*
|
||||
* 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 priority implements the priority balancer.
|
||||
//
|
||||
// This balancer will be kept in internal until we use it in the xds balancers,
|
||||
// and are confident its functionalities are stable. It will then be exported
|
||||
// for more users.
|
||||
package priority
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/base"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/internal/balancergroup"
|
||||
"google.golang.org/grpc/internal/buffer"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/hierarchy"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
// Name is the name of the priority balancer.
|
||||
const Name = "priority_experimental"
|
||||
|
||||
// DefaultSubBalancerCloseTimeout is defined as a variable instead of const for
|
||||
// testing.
|
||||
var DefaultSubBalancerCloseTimeout = 15 * time.Minute
|
||||
|
||||
func init() {
|
||||
balancer.Register(bb{})
|
||||
}
|
||||
|
||||
type bb struct{}
|
||||
|
||||
func (bb) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
|
||||
b := &priorityBalancer{
|
||||
cc: cc,
|
||||
done: grpcsync.NewEvent(),
|
||||
children: make(map[string]*childBalancer),
|
||||
childBalancerStateUpdate: buffer.NewUnbounded[any](),
|
||||
}
|
||||
|
||||
b.logger = prefixLogger(b)
|
||||
b.bg = balancergroup.New(balancergroup.Options{
|
||||
CC: cc,
|
||||
BuildOpts: bOpts,
|
||||
StateAggregator: b,
|
||||
Logger: b.logger,
|
||||
SubBalancerCloseTimeout: DefaultSubBalancerCloseTimeout,
|
||||
})
|
||||
go b.run()
|
||||
b.logger.Infof("Created")
|
||||
return b
|
||||
}
|
||||
|
||||
func (b bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
return parseConfig(s)
|
||||
}
|
||||
|
||||
func (bb) Name() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
// timerWrapper wraps a timer with a boolean. So that when a race happens
|
||||
// between AfterFunc and Stop, the func is guaranteed to not execute.
|
||||
type timerWrapper struct {
|
||||
stopped bool
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
type priorityBalancer struct {
|
||||
logger *grpclog.PrefixLogger
|
||||
cc balancer.ClientConn
|
||||
bg *balancergroup.BalancerGroup
|
||||
done *grpcsync.Event
|
||||
childBalancerStateUpdate *buffer.Unbounded[any]
|
||||
|
||||
mu sync.Mutex
|
||||
childInUse string
|
||||
// priorities is a list of child names from higher to lower priority.
|
||||
priorities []string
|
||||
// children is a map from child name to sub-balancers.
|
||||
children map[string]*childBalancer
|
||||
|
||||
// Set during UpdateClientConnState when calling into sub-balancers.
|
||||
// Prevents child updates from recomputing the active priority or sending
|
||||
// an update of the aggregated picker to the parent. Cleared after all
|
||||
// sub-balancers have finished UpdateClientConnState, after which
|
||||
// syncPriority is called manually.
|
||||
inhibitPickerUpdates bool
|
||||
}
|
||||
|
||||
func (b *priorityBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Received an update with balancer config: %+v", pretty.ToJSON(s.BalancerConfig))
|
||||
}
|
||||
newConfig, ok := s.BalancerConfig.(*LBConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected balancer config with type: %T", s.BalancerConfig)
|
||||
}
|
||||
endpointsSplit := hierarchy.Group(s.ResolverState.Endpoints)
|
||||
|
||||
b.mu.Lock()
|
||||
// Create and remove children, since we know all children from the config
|
||||
// are used by some priority.
|
||||
for name, newSubConfig := range newConfig.Children {
|
||||
bb := balancer.Get(newSubConfig.Config.Name)
|
||||
if bb == nil {
|
||||
b.logger.Errorf("balancer name %v from config is not registered", newSubConfig.Config.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
currentChild, ok := b.children[name]
|
||||
if !ok {
|
||||
// This is a new child, add it to the children list. But note that
|
||||
// the balancer isn't built, because this child can be a low
|
||||
// priority. If necessary, it will be built when syncing priorities.
|
||||
cb := newChildBalancer(name, b, bb.Name(), b.cc)
|
||||
cb.updateConfig(newSubConfig, resolver.State{
|
||||
Endpoints: endpointsSplit[name],
|
||||
ServiceConfig: s.ResolverState.ServiceConfig,
|
||||
Attributes: s.ResolverState.Attributes,
|
||||
})
|
||||
b.children[name] = cb
|
||||
continue
|
||||
}
|
||||
|
||||
// This is not a new child. But the config/addresses could change.
|
||||
|
||||
// The balancing policy name is changed, close the old child. But don't
|
||||
// rebuild, rebuild will happen when syncing priorities.
|
||||
if currentChild.balancerName != bb.Name() {
|
||||
currentChild.stop(true)
|
||||
currentChild.updateBalancerName(bb.Name())
|
||||
}
|
||||
|
||||
// Update config and address, but note that this doesn't send the
|
||||
// updates to non-started child balancers (the child balancer might not
|
||||
// be built, if it's a low priority).
|
||||
currentChild.updateConfig(newSubConfig, resolver.State{
|
||||
Endpoints: endpointsSplit[name],
|
||||
ServiceConfig: s.ResolverState.ServiceConfig,
|
||||
Attributes: s.ResolverState.Attributes,
|
||||
})
|
||||
}
|
||||
// Cleanup resources used by children removed from the config.
|
||||
for name, oldChild := range b.children {
|
||||
if _, ok := newConfig.Children[name]; !ok {
|
||||
oldChild.stop(!envconfig.EnablePriorityLBChildPolicyCache)
|
||||
delete(b.children, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Update priorities and handle priority changes.
|
||||
b.priorities = newConfig.Priorities
|
||||
|
||||
// Everything was removed by the update.
|
||||
if len(b.priorities) == 0 {
|
||||
b.childInUse = ""
|
||||
b.cc.UpdateState(balancer.State{
|
||||
ConnectivityState: connectivity.TransientFailure,
|
||||
Picker: base.NewErrPicker(ErrAllPrioritiesRemoved),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// This will sync the states of all children to the new updated
|
||||
// priorities. Includes starting/stopping child balancers when necessary.
|
||||
// Block picker updates until all children have had a chance to call
|
||||
// UpdateState to prevent races where, e.g., the active priority reports
|
||||
// transient failure but a higher priority may have reported something that
|
||||
// made it active, and if the transient failure update is handled first,
|
||||
// RPCs could fail.
|
||||
b.inhibitPickerUpdates = true
|
||||
// Add an item to queue to notify us when the current items in the queue
|
||||
// are done and syncPriority has been called.
|
||||
done := make(chan struct{})
|
||||
b.childBalancerStateUpdate.Put(resumePickerUpdates{done: done})
|
||||
b.mu.Unlock()
|
||||
<-done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *priorityBalancer) ResolverError(err error) {
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Received error from the resolver: %v", err)
|
||||
}
|
||||
b.bg.ResolverError(err)
|
||||
}
|
||||
|
||||
func (b *priorityBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
|
||||
b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
|
||||
}
|
||||
|
||||
func (b *priorityBalancer) Close() {
|
||||
b.bg.Close()
|
||||
b.childBalancerStateUpdate.Close()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.done.Fire()
|
||||
// Clear states of the current child in use, so if there's a race in picker
|
||||
// update, it will be dropped.
|
||||
b.childInUse = ""
|
||||
// Stop the child policies, this is necessary to stop the init timers in the
|
||||
// children.
|
||||
for _, child := range b.children {
|
||||
child.stop(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *priorityBalancer) ExitIdle() {
|
||||
b.bg.ExitIdle()
|
||||
}
|
||||
|
||||
// UpdateState implements balancergroup.BalancerStateAggregator interface. The
|
||||
// balancer group sends new connectivity state and picker here.
|
||||
func (b *priorityBalancer) UpdateState(childName string, state balancer.State) {
|
||||
b.childBalancerStateUpdate.Put(childBalancerState{
|
||||
name: childName,
|
||||
s: state,
|
||||
})
|
||||
}
|
||||
|
||||
type childBalancerState struct {
|
||||
name string
|
||||
s balancer.State
|
||||
}
|
||||
|
||||
type resumePickerUpdates struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// run handles child update in a separate goroutine, so if the child sends
|
||||
// updates inline (when called by parent), it won't cause deadlocks (by trying
|
||||
// to hold the same mutex).
|
||||
func (b *priorityBalancer) run() {
|
||||
for {
|
||||
select {
|
||||
case u, ok := <-b.childBalancerStateUpdate.Get():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
b.childBalancerStateUpdate.Load()
|
||||
// Needs to handle state update in a goroutine, because each state
|
||||
// update needs to start/close child policy, could result in
|
||||
// deadlock.
|
||||
b.mu.Lock()
|
||||
if b.done.HasFired() {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
switch s := u.(type) {
|
||||
case childBalancerState:
|
||||
b.handleChildStateUpdate(s.name, s.s)
|
||||
case resumePickerUpdates:
|
||||
b.inhibitPickerUpdates = false
|
||||
b.syncPriority(b.childInUse)
|
||||
close(s.done)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
case <-b.done.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
186
internal/xds/balancer/priority/balancer_child.go
Normal file
186
internal/xds/balancer/priority/balancer_child.go
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
*
|
||||
* 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 priority
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/base"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
var timeAfterFunc = time.AfterFunc
|
||||
|
||||
type childBalancer struct {
|
||||
name string
|
||||
parent *priorityBalancer
|
||||
parentCC balancer.ClientConn
|
||||
balancerName string
|
||||
cc *ignoreResolveNowClientConn
|
||||
|
||||
ignoreReresolutionRequests bool
|
||||
config serviceconfig.LoadBalancingConfig
|
||||
rState resolver.State
|
||||
|
||||
started bool
|
||||
// This is set when the child reports TransientFailure, and unset when it
|
||||
// reports Ready or Idle. It is used to decide whether the failover timer
|
||||
// should start when the child is transitioning into Connecting. The timer
|
||||
// will be restarted if the child has not reported TF more recently than it
|
||||
// reported Ready or Idle.
|
||||
reportedTF bool
|
||||
// The latest state the child balancer provided.
|
||||
state balancer.State
|
||||
// The timer to give a priority some time to connect. And if the priority
|
||||
// doesn't go into Ready/Failure, the next priority will be started.
|
||||
initTimer *timerWrapper
|
||||
}
|
||||
|
||||
// newChildBalancer creates a child balancer place holder, but doesn't
|
||||
// build/start the child balancer.
|
||||
func newChildBalancer(name string, parent *priorityBalancer, balancerName string, cc balancer.ClientConn) *childBalancer {
|
||||
return &childBalancer{
|
||||
name: name,
|
||||
parent: parent,
|
||||
parentCC: cc,
|
||||
balancerName: balancerName,
|
||||
cc: newIgnoreResolveNowClientConn(cc, false),
|
||||
started: false,
|
||||
// Start with the connecting state and picker with re-pick error, so
|
||||
// that when a priority switch causes this child picked before it's
|
||||
// balancing policy is created, a re-pick will happen.
|
||||
state: balancer.State{
|
||||
ConnectivityState: connectivity.Connecting,
|
||||
Picker: base.NewErrPicker(balancer.ErrNoSubConnAvailable),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// updateBalancerName updates balancer name for the child, but doesn't build a
|
||||
// new one. The parent priority LB always closes the child policy before
|
||||
// updating the balancer name, and the new balancer is built when it gets added
|
||||
// to the balancergroup as part of start().
|
||||
func (cb *childBalancer) updateBalancerName(balancerName string) {
|
||||
cb.balancerName = balancerName
|
||||
cb.cc = newIgnoreResolveNowClientConn(cb.parentCC, cb.ignoreReresolutionRequests)
|
||||
}
|
||||
|
||||
// updateConfig sets childBalancer's config and state, but doesn't send update to
|
||||
// the child balancer unless it is started.
|
||||
func (cb *childBalancer) updateConfig(child *Child, rState resolver.State) {
|
||||
cb.ignoreReresolutionRequests = child.IgnoreReresolutionRequests
|
||||
cb.config = child.Config.Config
|
||||
cb.rState = rState
|
||||
if cb.started {
|
||||
cb.sendUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// start builds the child balancer if it's not already started.
|
||||
//
|
||||
// It doesn't do it directly. It asks the balancer group to build it.
|
||||
func (cb *childBalancer) start() {
|
||||
if cb.started {
|
||||
return
|
||||
}
|
||||
cb.started = true
|
||||
cb.parent.bg.AddWithClientConn(cb.name, cb.balancerName, cb.cc)
|
||||
cb.startInitTimer()
|
||||
cb.sendUpdate()
|
||||
}
|
||||
|
||||
// sendUpdate sends the addresses and config to the child balancer.
|
||||
func (cb *childBalancer) sendUpdate() {
|
||||
cb.cc.updateIgnoreResolveNow(cb.ignoreReresolutionRequests)
|
||||
// TODO: return and aggregate the returned error in the parent.
|
||||
err := cb.parent.bg.UpdateClientConnState(cb.name, balancer.ClientConnState{
|
||||
ResolverState: cb.rState,
|
||||
BalancerConfig: cb.config,
|
||||
})
|
||||
// Report TF if update to the child fails.
|
||||
if err != nil {
|
||||
cb.parent.logger.Warningf("Failed to update state for child policy %q: %v", cb.name, err)
|
||||
cb.reportedTF = true
|
||||
cb.state = balancer.State{
|
||||
ConnectivityState: connectivity.TransientFailure,
|
||||
Picker: base.NewErrPicker(err),
|
||||
}
|
||||
cb.parent.handleChildStateUpdate(cb.name, cb.state)
|
||||
}
|
||||
}
|
||||
|
||||
// stop stops the child balancer and resets the state.
|
||||
//
|
||||
// It doesn't do it directly. It asks the balancer group to remove it.
|
||||
//
|
||||
// Note that the underlying balancer group could keep the child in a cache.
|
||||
func (cb *childBalancer) stop(immediate bool) {
|
||||
if !cb.started {
|
||||
return
|
||||
}
|
||||
cb.stopInitTimer()
|
||||
if immediate {
|
||||
cb.parent.bg.RemoveImmediately(cb.name)
|
||||
} else {
|
||||
cb.parent.bg.Remove(cb.name)
|
||||
}
|
||||
cb.started = false
|
||||
cb.state = balancer.State{
|
||||
ConnectivityState: connectivity.Connecting,
|
||||
Picker: base.NewErrPicker(balancer.ErrNoSubConnAvailable),
|
||||
}
|
||||
// Clear child.reportedTF, so that if this child is started later, it will
|
||||
// be given time to connect.
|
||||
cb.reportedTF = false
|
||||
}
|
||||
|
||||
func (cb *childBalancer) startInitTimer() {
|
||||
if cb.initTimer != nil {
|
||||
return
|
||||
}
|
||||
// Need this local variable to capture timerW in the AfterFunc closure
|
||||
// to check the stopped boolean.
|
||||
timerW := &timerWrapper{}
|
||||
cb.initTimer = timerW
|
||||
timerW.timer = timeAfterFunc(DefaultPriorityInitTimeout, func() {
|
||||
cb.parent.mu.Lock()
|
||||
defer cb.parent.mu.Unlock()
|
||||
if timerW.stopped {
|
||||
return
|
||||
}
|
||||
cb.initTimer = nil
|
||||
// Re-sync the priority. This will switch to the next priority if
|
||||
// there's any. Note that it's important sync() is called after setting
|
||||
// initTimer to nil.
|
||||
cb.parent.syncPriority("")
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *childBalancer) stopInitTimer() {
|
||||
timerW := cb.initTimer
|
||||
if timerW == nil {
|
||||
return
|
||||
}
|
||||
cb.initTimer = nil
|
||||
timerW.stopped = true
|
||||
timerW.timer.Stop()
|
||||
}
|
||||
223
internal/xds/balancer/priority/balancer_priority.go
Normal file
223
internal/xds/balancer/priority/balancer_priority.go
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
*
|
||||
* 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 priority
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAllPrioritiesRemoved is returned by the picker when there's no priority available.
|
||||
ErrAllPrioritiesRemoved = errors.New("no priority is provided, all priorities are removed")
|
||||
// DefaultPriorityInitTimeout is the timeout after which if a priority is
|
||||
// not READY, the next will be started. It's exported to be overridden by
|
||||
// tests.
|
||||
DefaultPriorityInitTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// syncPriority handles priority after a config update or a child balancer
|
||||
// connectivity state update. It makes sure the balancer state (started or not)
|
||||
// is in sync with the priorities (even in tricky cases where a child is moved
|
||||
// from a priority to another).
|
||||
//
|
||||
// It's guaranteed that after this function returns:
|
||||
//
|
||||
// If some child is READY, it is childInUse, and all lower priorities are
|
||||
// closed.
|
||||
//
|
||||
// If some child is newly started(in Connecting for the first time), it is
|
||||
// childInUse, and all lower priorities are closed.
|
||||
//
|
||||
// Otherwise, the lowest priority is childInUse (none of the children is
|
||||
// ready, and the overall state is not ready).
|
||||
//
|
||||
// Steps:
|
||||
//
|
||||
// If all priorities were deleted, unset childInUse (to an empty string), and
|
||||
// set parent ClientConn to TransientFailure
|
||||
//
|
||||
// Otherwise, Scan all children from p0, and check balancer stats:
|
||||
//
|
||||
// For any of the following cases:
|
||||
//
|
||||
// If balancer is not started (not built), this is either a new child with
|
||||
// high priority, or a new builder for an existing child.
|
||||
//
|
||||
// If balancer is Connecting and has non-nil initTimer (meaning it
|
||||
// transitioned from Ready or Idle to connecting, not from TF, so we
|
||||
// should give it init-time to connect).
|
||||
//
|
||||
// If balancer is READY or IDLE
|
||||
//
|
||||
// If this is the lowest priority
|
||||
//
|
||||
// do the following:
|
||||
//
|
||||
// if this is not the old childInUse, override picker so old picker is no
|
||||
// longer used.
|
||||
//
|
||||
// switch to it (because all higher priorities are neither new or Ready)
|
||||
//
|
||||
// forward the new addresses and config
|
||||
//
|
||||
// Caller must hold b.mu.
|
||||
func (b *priorityBalancer) syncPriority(childUpdating string) {
|
||||
if b.inhibitPickerUpdates {
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Skipping update from child policy %q", childUpdating)
|
||||
}
|
||||
return
|
||||
}
|
||||
for p, name := range b.priorities {
|
||||
child, ok := b.children[name]
|
||||
if !ok {
|
||||
b.logger.Warningf("Priority name %q is not found in list of child policies", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !child.started ||
|
||||
child.state.ConnectivityState == connectivity.Ready ||
|
||||
child.state.ConnectivityState == connectivity.Idle ||
|
||||
(child.state.ConnectivityState == connectivity.Connecting && child.initTimer != nil) ||
|
||||
p == len(b.priorities)-1 {
|
||||
if b.childInUse != child.name || child.name == childUpdating {
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("childInUse, childUpdating: %q, %q", b.childInUse, child.name)
|
||||
}
|
||||
// If we switch children or the child in use just updated its
|
||||
// picker, push the child's picker to the parent.
|
||||
b.cc.UpdateState(child.state)
|
||||
}
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Switching to (%q, %v) in syncPriority", child.name, p)
|
||||
}
|
||||
b.switchToChild(child, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop priorities [p+1, lowest].
|
||||
//
|
||||
// Caller must hold b.mu.
|
||||
func (b *priorityBalancer) stopSubBalancersLowerThanPriority(p int) {
|
||||
for i := p + 1; i < len(b.priorities); i++ {
|
||||
name := b.priorities[i]
|
||||
child, ok := b.children[name]
|
||||
if !ok {
|
||||
b.logger.Warningf("Priority name %q is not found in list of child policies", name)
|
||||
continue
|
||||
}
|
||||
child.stop(false)
|
||||
}
|
||||
}
|
||||
|
||||
// switchToChild does the following:
|
||||
// - stop all child with lower priorities
|
||||
// - if childInUse is not this child
|
||||
// - set childInUse to this child
|
||||
// - if this child is not started, start it
|
||||
//
|
||||
// Note that it does NOT send the current child state (picker) to the parent
|
||||
// ClientConn. The caller needs to send it if necessary.
|
||||
//
|
||||
// this can be called when
|
||||
// 1. first update, start p0
|
||||
// 2. an update moves a READY child from a lower priority to higher
|
||||
// 2. a different builder is updated for this child
|
||||
// 3. a high priority goes Failure, start next
|
||||
// 4. a high priority init timeout, start next
|
||||
//
|
||||
// Caller must hold b.mu.
|
||||
func (b *priorityBalancer) switchToChild(child *childBalancer, priority int) {
|
||||
// Stop lower priorities even if childInUse is same as this child. It's
|
||||
// possible this child was moved from a priority to another.
|
||||
b.stopSubBalancersLowerThanPriority(priority)
|
||||
|
||||
// If this child is already in use, do nothing.
|
||||
//
|
||||
// This can happen:
|
||||
// - all priorities are not READY, an config update always triggers switch
|
||||
// to the lowest. In this case, the lowest child could still be connecting,
|
||||
// so we don't stop the init timer.
|
||||
// - a high priority is READY, an config update always triggers switch to
|
||||
// it.
|
||||
if b.childInUse == child.name && child.started {
|
||||
return
|
||||
}
|
||||
b.childInUse = child.name
|
||||
|
||||
if !child.started {
|
||||
child.start()
|
||||
}
|
||||
}
|
||||
|
||||
// handleChildStateUpdate start/close priorities based on the connectivity
|
||||
// state.
|
||||
func (b *priorityBalancer) handleChildStateUpdate(childName string, newState balancer.State) {
|
||||
// Update state in child. The updated picker will be sent to parent later if
|
||||
// necessary.
|
||||
child, ok := b.children[childName]
|
||||
if !ok {
|
||||
b.logger.Warningf("Child policy not found for %q", childName)
|
||||
return
|
||||
}
|
||||
if !child.started {
|
||||
b.logger.Warningf("Ignoring update from child policy %q which is not in started state: %+v", childName, newState)
|
||||
return
|
||||
}
|
||||
oldState := child.state
|
||||
child.state = newState
|
||||
|
||||
// We start/stop the init timer of this child based on the new connectivity
|
||||
// state. syncPriority() later will need the init timer (to check if it's
|
||||
// nil or not) to decide which child to switch to.
|
||||
switch newState.ConnectivityState {
|
||||
case connectivity.Ready, connectivity.Idle:
|
||||
child.reportedTF = false
|
||||
child.stopInitTimer()
|
||||
case connectivity.TransientFailure:
|
||||
child.reportedTF = true
|
||||
child.stopInitTimer()
|
||||
case connectivity.Connecting:
|
||||
// The init timer is created when the child is created and is reset when
|
||||
// it reports Ready or Idle. Most child policies start off in
|
||||
// Connecting, but ring_hash starts off in Idle and moves to Connecting
|
||||
// when a request comes in. To support such cases, we restart the init
|
||||
// timer when we see Connecting, but only if the child has not reported
|
||||
// TransientFailure more recently than it reported Ready or Idle. See
|
||||
// gRFC A42 for details on why ring_hash is special and what provisions
|
||||
// are required to make it work as a child of the priority LB policy.
|
||||
//
|
||||
// We don't want to restart the timer if the child was already in
|
||||
// Connecting, because we want failover to happen once the timer elapses
|
||||
// even when the child is still in Connecting.
|
||||
if !child.reportedTF && oldState.ConnectivityState != connectivity.Connecting {
|
||||
child.startInitTimer()
|
||||
}
|
||||
default:
|
||||
// New state is Shutdown, should never happen. Don't forward.
|
||||
}
|
||||
|
||||
child.parent.syncPriority(childName)
|
||||
}
|
||||
2366
internal/xds/balancer/priority/balancer_test.go
Normal file
2366
internal/xds/balancer/priority/balancer_test.go
Normal file
File diff suppressed because it is too large
Load Diff
67
internal/xds/balancer/priority/config.go
Normal file
67
internal/xds/balancer/priority/config.go
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 priority
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
// Child is a child of priority balancer.
|
||||
type Child struct {
|
||||
Config *internalserviceconfig.BalancerConfig `json:"config,omitempty"`
|
||||
IgnoreReresolutionRequests bool `json:"ignoreReresolutionRequests,omitempty"`
|
||||
}
|
||||
|
||||
// LBConfig represents priority balancer's config.
|
||||
type LBConfig struct {
|
||||
serviceconfig.LoadBalancingConfig `json:"-"`
|
||||
|
||||
// Children is a map from the child balancer names to their configs. Child
|
||||
// names can be found in field Priorities.
|
||||
Children map[string]*Child `json:"children,omitempty"`
|
||||
// Priorities is a list of child balancer names. They are sorted from
|
||||
// highest priority to low. The type/config for each child can be found in
|
||||
// field Children, with the balancer name as the key.
|
||||
Priorities []string `json:"priorities,omitempty"`
|
||||
}
|
||||
|
||||
func parseConfig(c json.RawMessage) (*LBConfig, error) {
|
||||
var cfg LBConfig
|
||||
if err := json.Unmarshal(c, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prioritiesSet := make(map[string]bool)
|
||||
for _, name := range cfg.Priorities {
|
||||
if _, ok := cfg.Children[name]; !ok {
|
||||
return nil, fmt.Errorf("LB policy name %q found in Priorities field (%v) is not found in Children field (%+v)", name, cfg.Priorities, cfg.Children)
|
||||
}
|
||||
prioritiesSet[name] = true
|
||||
}
|
||||
for name := range cfg.Children {
|
||||
if _, ok := prioritiesSet[name]; !ok {
|
||||
return nil, fmt.Errorf("LB policy name %q found in Children field (%v) is not found in Priorities field (%+v)", name, cfg.Children, cfg.Priorities)
|
||||
}
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
108
internal/xds/balancer/priority/config_test.go
Normal file
108
internal/xds/balancer/priority/config_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 priority
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/balancer/roundrobin"
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
js string
|
||||
want *LBConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "child not found",
|
||||
js: `{
|
||||
"priorities": ["child-1", "child-2", "child-3"],
|
||||
"children": {
|
||||
"child-1": {"config": [{"round_robin":{}}]},
|
||||
"child-3": {"config": [{"round_robin":{}}]}
|
||||
}
|
||||
}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "child not used",
|
||||
js: `{
|
||||
"priorities": ["child-1", "child-2"],
|
||||
"children": {
|
||||
"child-1": {"config": [{"round_robin":{}}]},
|
||||
"child-2": {"config": [{"round_robin":{}}]},
|
||||
"child-3": {"config": [{"round_robin":{}}]}
|
||||
}
|
||||
}
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "good",
|
||||
js: `{
|
||||
"priorities": ["child-1", "child-2", "child-3"],
|
||||
"children": {
|
||||
"child-1": {"config": [{"round_robin":{}}], "ignoreReresolutionRequests": true},
|
||||
"child-2": {"config": [{"round_robin":{}}]},
|
||||
"child-3": {"config": [{"round_robin":{}}]}
|
||||
}
|
||||
}
|
||||
`,
|
||||
want: &LBConfig{
|
||||
Children: map[string]*Child{
|
||||
"child-1": {
|
||||
Config: &internalserviceconfig.BalancerConfig{
|
||||
Name: roundrobin.Name,
|
||||
},
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"child-2": {
|
||||
Config: &internalserviceconfig.BalancerConfig{
|
||||
Name: roundrobin.Name,
|
||||
},
|
||||
},
|
||||
"child-3": {
|
||||
Config: &internalserviceconfig.BalancerConfig{
|
||||
Name: roundrobin.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
Priorities: []string{"child-1", "child-2", "child-3"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseConfig([]byte(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 = %v, want %v, diff: %s", got, tt.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
internal/xds/balancer/priority/ignore_resolve_now.go
Normal file
50
internal/xds/balancer/priority/ignore_resolve_now.go
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
*
|
||||
* 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 priority
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// ignoreResolveNowClientConn wraps a balancer.ClientConn and overrides the
|
||||
// ResolveNow() method to ignore those calls if the ignoreResolveNow bit is set.
|
||||
type ignoreResolveNowClientConn struct {
|
||||
balancer.ClientConn
|
||||
ignoreResolveNow atomic.Bool
|
||||
}
|
||||
|
||||
func newIgnoreResolveNowClientConn(cc balancer.ClientConn, ignore bool) *ignoreResolveNowClientConn {
|
||||
ret := &ignoreResolveNowClientConn{ClientConn: cc}
|
||||
ret.updateIgnoreResolveNow(ignore)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (i *ignoreResolveNowClientConn) updateIgnoreResolveNow(b bool) {
|
||||
i.ignoreResolveNow.Store(b)
|
||||
}
|
||||
|
||||
func (i *ignoreResolveNowClientConn) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
if i.ignoreResolveNow.Load() {
|
||||
return
|
||||
}
|
||||
i.ClientConn.ResolveNow(o)
|
||||
}
|
||||
66
internal/xds/balancer/priority/ignore_resolve_now_test.go
Normal file
66
internal/xds/balancer/priority/ignore_resolve_now_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
*
|
||||
* 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 priority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
func (s) TestIgnoreResolveNowClientConn(t *testing.T) {
|
||||
cc := testutils.NewBalancerClientConn(t)
|
||||
ignoreCC := newIgnoreResolveNowClientConn(cc, false)
|
||||
|
||||
// Call ResolveNow() on the CC, it should be forwarded.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
ignoreCC.ResolveNow(resolver.ResolveNowOptions{})
|
||||
select {
|
||||
case <-cc.ResolveNowCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for ResolveNow()")
|
||||
}
|
||||
|
||||
// Update ignoreResolveNow to true, call ResolveNow() on the CC, they should
|
||||
// all be ignored.
|
||||
ignoreCC.updateIgnoreResolveNow(true)
|
||||
for i := 0; i < 5; i++ {
|
||||
ignoreCC.ResolveNow(resolver.ResolveNowOptions{})
|
||||
}
|
||||
select {
|
||||
case <-cc.ResolveNowCh:
|
||||
t.Fatalf("got unexpected ResolveNow() call")
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Update ignoreResolveNow to false, new ResolveNow() calls should be
|
||||
// forwarded.
|
||||
ignoreCC.updateIgnoreResolveNow(false)
|
||||
ignoreCC.ResolveNow(resolver.ResolveNowOptions{})
|
||||
select {
|
||||
case <-cc.ResolveNowCh:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timeout waiting for ResolveNow()")
|
||||
}
|
||||
}
|
||||
34
internal/xds/balancer/priority/logging.go
Normal file
34
internal/xds/balancer/priority/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 priority
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
const prefix = "[priority-lb %p] "
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
func prefixLogger(p *priorityBalancer) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
|
||||
}
|
||||
Reference in New Issue
Block a user