Skip to content

Commit 9b39ae2

Browse files
authored
priority: prevent init timer restarting when a child transitions from CONNECTING to CONNECTING (#8813)
Fixes #8516 This PR ensures that the init timer is not restarted when a child policy transitions from CONNECTING to CONNECTING. This ensures that we will only ever wait for `DefaultPriorityInitTimeout` which is set to a value of `10s` for a given child to become `Ready` before we attempt a failover to the next highest priority child. RELEASE NOTES: - xds/priority: Fixed a bug causing delayed failover to lower-priority clusters when a higher-priority cluster is stuck in the CONNECTING state.
1 parent e05f643 commit 9b39ae2

File tree

3 files changed

+124
-6
lines changed

3 files changed

+124
-6
lines changed

internal/xds/balancer/priority/balancer_child.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"google.golang.org/grpc/serviceconfig"
2929
)
3030

31+
var timeAfterFunc = time.AfterFunc
32+
3133
type childBalancer struct {
3234
name string
3335
parent *priorityBalancer
@@ -148,7 +150,7 @@ func (cb *childBalancer) startInitTimer() {
148150
// to check the stopped boolean.
149151
timerW := &timerWrapper{}
150152
cb.initTimer = timerW
151-
timerW.timer = time.AfterFunc(DefaultPriorityInitTimeout, func() {
153+
timerW.timer = timeAfterFunc(DefaultPriorityInitTimeout, func() {
152154
cb.parent.mu.Lock()
153155
defer cb.parent.mu.Unlock()
154156
if timerW.stopped {

internal/xds/balancer/priority/balancer_priority.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func (b *priorityBalancer) switchToChild(child *childBalancer, priority int) {
174174

175175
// handleChildStateUpdate start/close priorities based on the connectivity
176176
// state.
177-
func (b *priorityBalancer) handleChildStateUpdate(childName string, s balancer.State) {
177+
func (b *priorityBalancer) handleChildStateUpdate(childName string, newState balancer.State) {
178178
// Update state in child. The updated picker will be sent to parent later if
179179
// necessary.
180180
child, ok := b.children[childName]
@@ -183,23 +183,36 @@ func (b *priorityBalancer) handleChildStateUpdate(childName string, s balancer.S
183183
return
184184
}
185185
if !child.started {
186-
b.logger.Warningf("Ignoring update from child policy %q which is not in started state: %+v", childName, s)
186+
b.logger.Warningf("Ignoring update from child policy %q which is not in started state: %+v", childName, newState)
187187
return
188188
}
189-
child.state = s
189+
oldState := child.state
190+
child.state = newState
190191

191192
// We start/stop the init timer of this child based on the new connectivity
192193
// state. syncPriority() later will need the init timer (to check if it's
193194
// nil or not) to decide which child to switch to.
194-
switch s.ConnectivityState {
195+
switch newState.ConnectivityState {
195196
case connectivity.Ready, connectivity.Idle:
196197
child.reportedTF = false
197198
child.stopInitTimer()
198199
case connectivity.TransientFailure:
199200
child.reportedTF = true
200201
child.stopInitTimer()
201202
case connectivity.Connecting:
202-
if !child.reportedTF {
203+
// The init timer is created when the child is created and is reset when
204+
// it reports Ready or Idle. Most child policies start off in
205+
// Connecting, but ring_hash starts off in Idle and moves to Connecting
206+
// when a request comes in. To support such cases, we restart the init
207+
// timer when we see Connecting, but only if the child has not reported
208+
// TransientFailure more recently than it reported Ready or Idle. See
209+
// gRFC A42 for details on why ring_hash is special and what provisions
210+
// are required to make it work as a child of the priority LB policy.
211+
//
212+
// We don't want to restart the timer if the child was already in
213+
// Connecting, because we want failover to happen once the timer elapses
214+
// even when the child is still in Connecting.
215+
if !child.reportedTF && oldState.ConnectivityState != connectivity.Connecting {
203216
child.startInitTimer()
204217
}
205218
default:

internal/xds/balancer/priority/balancer_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,3 +2098,106 @@ func (s) TestPriority_HighPriorityUpdatesWhenLowInUse(t *testing.T) {
20982098
t.Fatal(err.Error())
20992099
}
21002100
}
2101+
2102+
// Tests that the priority balancer's init timer is not restarted when its child
2103+
// reports a state transition from CONNECTING to CONNECTING.
2104+
func (s) TestPriority_InitTimerNotRestarted_OnConnectingToConnecting(t *testing.T) {
2105+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
2106+
defer cancel()
2107+
2108+
initTimerStarted := make(chan struct{}, 1)
2109+
origTimeAfterFunc := timeAfterFunc
2110+
timeAfterFunc = func(d time.Duration, f func()) *time.Timer {
2111+
select {
2112+
case initTimerStarted <- struct{}{}:
2113+
case <-ctx.Done():
2114+
t.Errorf("Timeout waiting to send init timer started signal")
2115+
}
2116+
return time.AfterFunc(d, f)
2117+
}
2118+
defer func() { timeAfterFunc = origTimeAfterFunc }()
2119+
2120+
cc := testutils.NewBalancerClientConn(t)
2121+
bb := balancer.Get(Name)
2122+
pb := bb.Build(cc, balancer.BuildOptions{})
2123+
defer pb.Close()
2124+
2125+
// One child, with two backends.
2126+
ccs := balancer.ClientConnState{
2127+
ResolverState: resolver.State{
2128+
Endpoints: []resolver.Endpoint{
2129+
hierarchy.SetInEndpoint(resolver.Endpoint{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}}, []string{"child-0"}),
2130+
hierarchy.SetInEndpoint(resolver.Endpoint{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[1]}}}, []string{"child-0"}),
2131+
},
2132+
},
2133+
BalancerConfig: &LBConfig{
2134+
Children: map[string]*Child{
2135+
"child-0": {Config: &internalserviceconfig.BalancerConfig{Name: roundrobin.Name}},
2136+
},
2137+
Priorities: []string{"child-0"},
2138+
},
2139+
}
2140+
if err := pb.UpdateClientConnState(ccs); err != nil {
2141+
t.Fatalf("UpdateClientConnState(%+v) failed: %v", ccs, err)
2142+
}
2143+
2144+
// Wait for child-0 to be started and two subchannels to be created.
2145+
var sc0, sc1 *testutils.TestSubConn
2146+
for i := range 2 {
2147+
var addrs []resolver.Address
2148+
select {
2149+
case addrs = <-cc.NewSubConnAddrsCh:
2150+
case <-ctx.Done():
2151+
t.Fatalf("Timeout waiting for subconn %d to be created", i)
2152+
}
2153+
switch got := addrs[0].Addr; got {
2154+
case testBackendAddrStrs[0]:
2155+
sc0 = <-cc.NewSubConnCh
2156+
case testBackendAddrStrs[1]:
2157+
sc1 = <-cc.NewSubConnCh
2158+
default:
2159+
t.Fatalf("Got unexpected new subconn addr: %q, want %q or %q", got, testBackendAddrStrs[0], testBackendAddrStrs[1])
2160+
}
2161+
}
2162+
2163+
// Move both subchannels to CONNECTING.
2164+
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
2165+
sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
2166+
2167+
// Ensure that the init timer is started only once.
2168+
select {
2169+
case <-initTimerStarted:
2170+
case <-ctx.Done():
2171+
t.Fatalf("Timeout waiting for init timer to start")
2172+
}
2173+
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
2174+
defer sCancel()
2175+
select {
2176+
case <-initTimerStarted:
2177+
t.Fatalf("Init timer started when second subchannel moved to CONNECTING")
2178+
case <-sCtx.Done():
2179+
}
2180+
2181+
// Simulate the connection succeeding (subchannel becoming Ready), and then
2182+
// failing (subchannel moving to Idle). RR will immediately start connecting
2183+
// on the failed subchannel, and will therefore reporting an overall state
2184+
// of Connecting. This should trigger a restart of the init timer.
2185+
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
2186+
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Idle})
2187+
select {
2188+
case <-initTimerStarted:
2189+
case <-ctx.Done():
2190+
t.Fatalf("Timeout waiting for init timer to start")
2191+
}
2192+
2193+
// Move the subchannel to CONNECTING again, and ensure that the init timer
2194+
// is not restarted.
2195+
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
2196+
sCtx, sCancel = context.WithTimeout(ctx, defaultTestShortTimeout)
2197+
defer sCancel()
2198+
select {
2199+
case <-initTimerStarted:
2200+
t.Fatalf("Init timer restarted when subchannel moved from Ready to Idle")
2201+
case <-sCtx.Done():
2202+
}
2203+
}

0 commit comments

Comments
 (0)