util: Graceful switch to new LB when leaving CONNECTING

Previously it would wait for the new LB to enter READY. However, that
prevents there being an upper-bound on how long the old policy will
continue to be used. The point of graceful switch is to avoid RPCs
seeing increased latency when we swap config. We don't want it to
prevent the system from becoming eventually consistent.
This commit is contained in:
Eric Anderson 2025-03-27 13:52:30 -07:00
parent 7507a9ec06
commit 2e260a4bbc
2 changed files with 24 additions and 7 deletions

View File

@ -38,7 +38,8 @@ import javax.annotation.concurrent.NotThreadSafe;
/**
* A load balancer that gracefully swaps to a new lb policy. If the channel is currently in a state
* other than READY, the new policy will be swapped into place immediately. Otherwise, the channel
* will keep using the old policy until the new policy reports READY or the old policy exits READY.
* will keep using the old policy until the new policy leaves CONNECTING or the old policy exits
* READY.
*
* <p>The child balancer and configuration is specified using service config. Config objects are
* generally created by calling {@link #parseLoadBalancingPolicyConfig(List)} from a
@ -147,7 +148,7 @@ public final class GracefulSwitchLoadBalancer extends ForwardingLoadBalancer {
checkState(currentLbIsReady, "there's pending lb while current lb has been out of READY");
pendingState = newState;
pendingPicker = newPicker;
if (newState == ConnectivityState.READY) {
if (newState != ConnectivityState.CONNECTING) {
swap();
}
} else if (lb == currentLb) {

View File

@ -18,6 +18,7 @@ package io.grpc.util;
import static com.google.common.truth.Truth.assertThat;
import static io.grpc.ConnectivityState.CONNECTING;
import static io.grpc.ConnectivityState.IDLE;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
import static io.grpc.util.GracefulSwitchLoadBalancer.BUFFER_PICKER;
@ -32,6 +33,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.common.testing.EqualsTester;
import io.grpc.ConnectivityState;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
@ -363,7 +365,21 @@ public class GracefulSwitchLoadBalancerTest {
}
@Test
public void updateBalancingStateIsGraceful() {
public void updateBalancingStateIsGraceful_Ready() {
updateBalancingStateIsGraceful(READY);
}
@Test
public void updateBalancingStateIsGraceful_TransientFailure() {
updateBalancingStateIsGraceful(TRANSIENT_FAILURE);
}
@Test
public void updateBalancingStateIsGraceful_Idle() {
updateBalancingStateIsGraceful(IDLE);
}
public void updateBalancingStateIsGraceful(ConnectivityState swapsOnState) {
assertIsOk(gracefulSwitchLb.acceptResolvedAddresses(addressesBuilder()
.setLoadBalancingPolicyConfig(createConfig(lbPolicies[0], new Object()))
.build()));
@ -392,11 +408,11 @@ public class GracefulSwitchLoadBalancerTest {
helper2.updateBalancingState(CONNECTING, picker);
verify(mockHelper, never()).updateBalancingState(CONNECTING, picker);
// lb2 reports READY
// lb2 reports swapsOnState
SubchannelPicker picker2 = mock(SubchannelPicker.class);
helper2.updateBalancingState(READY, picker2);
helper2.updateBalancingState(swapsOnState, picker2);
verify(lb0).shutdown();
verify(mockHelper).updateBalancingState(READY, picker2);
verify(mockHelper).updateBalancingState(swapsOnState, picker2);
assertIsOk(gracefulSwitchLb.acceptResolvedAddresses(addressesBuilder()
.setLoadBalancingPolicyConfig(createConfig(lbPolicies[3], new Object()))
@ -407,7 +423,7 @@ public class GracefulSwitchLoadBalancerTest {
helper3.updateBalancingState(CONNECTING, picker3);
verify(mockHelper, never()).updateBalancingState(CONNECTING, picker3);
// lb2 out of READY
// lb2 out of swapsOnState
picker2 = mock(SubchannelPicker.class);
helper2.updateBalancingState(CONNECTING, picker2);
verify(mockHelper, never()).updateBalancingState(CONNECTING, picker2);