grpclb: keep track of state updates for cached Subchannels. (#5441)

The problem: GrpclbState tracks Subchannels' states as a mutable
attribute in Subchannel.getAttributes(). However, GrpclbState only
update this attribute for the Subchannels its managing. For those
cached in SubchannelPool, their state attributes are stale. When they
are given back to GrpclbState, IDLE state is assumed.  As a result, if
a Subchannel is READY when it's reclaimed from the pool, it will not
be picked.

To fix that, this change expands SubchannelPool interface to handle
Subchannel state updates, which GrpclbState will call. SubchannelPool
saves the latest state and delivers it when it's returned to
GrpclbState by scheduling a call to handleSubchannelState() in the
SynchronizationContext, so that GrpclbState will take the latest state
as if it was just reported from the Channel.
This commit is contained in:
Kun Zhang 2019-03-07 13:14:21 -08:00 committed by GitHub
parent 6b0325c84f
commit 034675e555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 177 additions and 45 deletions

View File

@ -21,7 +21,9 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Attributes;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancer.Subchannel;
import io.grpc.SynchronizationContext.ScheduledHandle;
@ -37,31 +39,51 @@ final class CachedSubchannelPool implements SubchannelPool {
new HashMap<>();
private Helper helper;
private LoadBalancer lb;
@VisibleForTesting
static final long SHUTDOWN_TIMEOUT_MS = 10000;
@Override
public void init(Helper helper) {
public void init(Helper helper, LoadBalancer lb) {
this.helper = checkNotNull(helper, "helper");
this.lb = checkNotNull(lb, "lb");
}
@Override
public Subchannel takeOrCreateSubchannel(
EquivalentAddressGroup eag, Attributes defaultAttributes) {
CacheEntry entry = cache.remove(eag);
Subchannel subchannel;
final CacheEntry entry = cache.remove(eag);
final Subchannel subchannel;
if (entry == null) {
subchannel = helper.createSubchannel(eag, defaultAttributes);
} else {
subchannel = entry.subchannel;
entry.shutdownTimer.cancel();
// Make the balancer up-to-date with the latest state in case it has changed while it's
// in the cache.
helper.getSynchronizationContext().execute(new Runnable() {
@Override
public void run() {
lb.handleSubchannelState(subchannel, entry.state);
}
});
}
return subchannel;
}
@Override
public void returnSubchannel(Subchannel subchannel) {
public void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo newStateInfo) {
CacheEntry cached = cache.get(subchannel.getAddresses());
if (cached == null || cached.subchannel != subchannel) {
// Given subchannel is not cached. Not our responsibility.
return;
}
cached.state = newStateInfo;
}
@Override
public void returnSubchannel(Subchannel subchannel, ConnectivityStateInfo lastKnownState) {
CacheEntry prev = cache.get(subchannel.getAddresses());
if (prev != null) {
// Returning the same Subchannel twice has no effect.
@ -77,7 +99,7 @@ final class CachedSubchannelPool implements SubchannelPool {
helper.getSynchronizationContext().schedule(
shutdownTask, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS,
helper.getScheduledExecutorService());
CacheEntry entry = new CacheEntry(subchannel, shutdownTimer);
CacheEntry entry = new CacheEntry(subchannel, shutdownTimer, lastKnownState);
cache.put(subchannel.getAddresses(), entry);
}
@ -110,10 +132,12 @@ final class CachedSubchannelPool implements SubchannelPool {
private static class CacheEntry {
final Subchannel subchannel;
final ScheduledHandle shutdownTimer;
ConnectivityStateInfo state;
CacheEntry(Subchannel subchannel, ScheduledHandle shutdownTimer) {
CacheEntry(Subchannel subchannel, ScheduledHandle shutdownTimer, ConnectivityStateInfo state) {
this.subchannel = checkNotNull(subchannel, "subchannel");
this.shutdownTimer = checkNotNull(shutdownTimer, "shutdownTimer");
this.state = checkNotNull(state, "state");
}
}
}

View File

@ -71,7 +71,7 @@ class GrpclbLoadBalancer extends LoadBalancer {
this.time = checkNotNull(time, "time provider");
this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
this.subchannelPool = checkNotNull(subchannelPool, "subchannelPool");
this.subchannelPool.init(helper);
this.subchannelPool.init(helper, this);
recreateStates();
checkNotNull(grpclbState, "grpclbState");
}

View File

@ -159,7 +159,8 @@ final class GrpclbState {
this.mode = checkNotNull(mode, "mode");
this.helper = checkNotNull(helper, "helper");
this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext");
this.subchannelPool = checkNotNull(subchannelPool, "subchannelPool");
this.subchannelPool =
mode == Mode.ROUND_ROBIN ? checkNotNull(subchannelPool, "subchannelPool") : null;
this.time = checkNotNull(time, "time provider");
this.timerService = checkNotNull(helper.getScheduledExecutorService(), "timerService");
this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider");
@ -168,7 +169,13 @@ final class GrpclbState {
}
void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo newState) {
if (newState.getState() == SHUTDOWN || !subchannels.values().contains(subchannel)) {
if (newState.getState() == SHUTDOWN) {
return;
}
if (!subchannels.values().contains(subchannel)) {
if (subchannelPool != null ) {
subchannelPool.handleSubchannelState(subchannel, newState);
}
return;
}
if (mode == Mode.ROUND_ROBIN && newState.getState() == IDLE) {
@ -311,8 +318,9 @@ final class GrpclbState {
// We close the subchannels through subchannelPool instead of helper just for convenience of
// testing.
for (Subchannel subchannel : subchannels.values()) {
subchannelPool.returnSubchannel(subchannel);
returnSubchannelToPool(subchannel);
}
subchannelPool.clear();
break;
case PICK_FIRST:
checkState(subchannels.size() == 1, "Excessive Subchannels: %s", subchannels);
@ -322,7 +330,6 @@ final class GrpclbState {
throw new AssertionError("Missing case for " + mode);
}
subchannels = Collections.emptyMap();
subchannelPool.clear();
cancelFallbackTimer();
cancelLbRpcRetryTimer();
}
@ -335,6 +342,10 @@ final class GrpclbState {
}
}
private void returnSubchannelToPool(Subchannel subchannel) {
subchannelPool.returnSubchannel(subchannel, subchannel.getAttributes().get(STATE_INFO).get());
}
@VisibleForTesting
@Nullable
GrpclbClientLoadRecorder getLoadRecorder() {
@ -383,7 +394,7 @@ final class GrpclbState {
for (Entry<List<EquivalentAddressGroup>, Subchannel> entry : subchannels.entrySet()) {
List<EquivalentAddressGroup> eagList = entry.getKey();
if (!newSubchannelMap.containsKey(eagList)) {
subchannelPool.returnSubchannel(entry.getValue());
returnSubchannelToPool(entry.getValue());
}
}
subchannels = Collections.unmodifiableMap(newSubchannelMap);

View File

@ -17,7 +17,9 @@
package io.grpc.grpclb;
import io.grpc.Attributes;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancer.Subchannel;
import javax.annotation.concurrent.NotThreadSafe;
@ -30,9 +32,9 @@ import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
interface SubchannelPool {
/**
* Pass essential utilities.
* Pass essential utilities and the balancer that's using this pool.
*/
void init(Helper helper);
void init(Helper helper, LoadBalancer lb);
/**
* Takes a {@link Subchannel} from the pool for the given {@code eag} if there is one available.
@ -42,10 +44,16 @@ interface SubchannelPool {
Subchannel takeOrCreateSubchannel(EquivalentAddressGroup eag, Attributes defaultAttributes);
/**
* Puts a {@link Subchannel} back to the pool. From this point the Subchannel is owned by the
* pool.
* Gets notified about a state change of Subchannel that is possibly cached in this pool. Do
* nothing if this pool doesn't own this Subchannel.
*/
void returnSubchannel(Subchannel subchannel);
void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo newStateInfo);
/**
* Puts a {@link Subchannel} back to the pool. From this point the Subchannel is owned by the
* pool, and the caller should stop referencing to this Subchannel.
*/
void returnSubchannel(Subchannel subchannel, ConnectivityStateInfo lastKnownState);
/**
* Shuts down all subchannels in the pool immediately.

View File

@ -21,6 +21,7 @@ import static io.grpc.grpclb.CachedSubchannelPool.SHUTDOWN_TIMEOUT_MS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@ -28,12 +29,17 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import io.grpc.Attributes;
import io.grpc.ConnectivityState;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancer.Subchannel;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
import io.grpc.grpclb.CachedSubchannelPool.ShutdownSubchannelTask;
import io.grpc.internal.FakeClock;
@ -58,6 +64,10 @@ public class CachedSubchannelPoolTest {
private static final Attributes.Key<String> ATTR_KEY = Attributes.Key.create("test-attr");
private static final Attributes ATTRS1 = Attributes.newBuilder().set(ATTR_KEY, "1").build();
private static final Attributes ATTRS2 = Attributes.newBuilder().set(ATTR_KEY, "2").build();
private static final ConnectivityStateInfo READY_STATE =
ConnectivityStateInfo.forNonError(ConnectivityState.READY);
private static final ConnectivityStateInfo TRANSIENT_FAILURE_STATE =
ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("Simulated"));
private static final FakeClock.TaskFilter SHUTDOWN_TASK_FILTER =
new FakeClock.TaskFilter() {
@Override
@ -69,6 +79,7 @@ public class CachedSubchannelPoolTest {
};
private final Helper helper = mock(Helper.class);
private final LoadBalancer balancer = mock(LoadBalancer.class);
private final FakeClock clock = new FakeClock();
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@ -96,9 +107,17 @@ public class CachedSubchannelPoolTest {
return subchannel;
}
}).when(helper).createSubchannel(any(List.class), any(Attributes.class));
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
syncContext.throwIfNotInThisSynchronizationContext();
return null;
}
}).when(balancer).handleSubchannelState(
any(Subchannel.class), any(ConnectivityStateInfo.class));
when(helper.getSynchronizationContext()).thenReturn(syncContext);
when(helper.getScheduledExecutorService()).thenReturn(clock.getScheduledExecutorService());
pool.init(helper);
pool.init(helper, balancer);
}
@After
@ -107,6 +126,9 @@ public class CachedSubchannelPoolTest {
for (Subchannel subchannel : mockSubchannels) {
verify(subchannel, atMost(1)).shutdown();
}
verify(balancer, atLeast(0))
.handleSubchannelState(any(Subchannel.class), any(ConnectivityStateInfo.class));
verifyNoMoreInteractions(balancer);
}
@Test
@ -120,13 +142,13 @@ public class CachedSubchannelPoolTest {
assertThat(subchannel2).isNotSameAs(subchannel1);
verify(helper).createSubchannel(eq(Arrays.asList(EAG2)), same(ATTRS2));
pool.returnSubchannel(subchannel1);
pool.returnSubchannel(subchannel1, READY_STATE);
// subchannel1 is 1ms away from expiration.
clock.forwardTime(SHUTDOWN_TIMEOUT_MS - 1, MILLISECONDS);
verify(subchannel1, never()).shutdown();
pool.returnSubchannel(subchannel2);
pool.returnSubchannel(subchannel2, READY_STATE);
// subchannel1 expires. subchannel2 is (SHUTDOWN_TIMEOUT_MS - 1) away from expiration.
clock.forwardTime(1, MILLISECONDS);
@ -150,7 +172,7 @@ public class CachedSubchannelPoolTest {
assertThat(subchannel2).isNotSameAs(subchannel1);
verify(helper).createSubchannel(eq(Arrays.asList(EAG2)), same(ATTRS2));
pool.returnSubchannel(subchannel1);
pool.returnSubchannel(subchannel1, READY_STATE);
// subchannel1 is 1ms away from expiration.
clock.forwardTime(SHUTDOWN_TIMEOUT_MS - 1, MILLISECONDS);
@ -159,7 +181,7 @@ public class CachedSubchannelPoolTest {
Subchannel subchannel1a = pool.takeOrCreateSubchannel(EAG1, ATTRS1);
assertThat(subchannel1a).isSameAs(subchannel1);
pool.returnSubchannel(subchannel2);
pool.returnSubchannel(subchannel2, READY_STATE);
// subchannel2 expires SHUTDOWN_TIMEOUT_MS after being returned
clock.forwardTime(SHUTDOWN_TIMEOUT_MS - 1, MILLISECONDS);
@ -173,7 +195,7 @@ public class CachedSubchannelPoolTest {
verify(helper, times(2)).createSubchannel(eq(Arrays.asList(EAG2)), same(ATTRS2));
// subchannel1 expires SHUTDOWN_TIMEOUT_MS after being returned
pool.returnSubchannel(subchannel1a);
pool.returnSubchannel(subchannel1a, READY_STATE);
clock.forwardTime(SHUTDOWN_TIMEOUT_MS - 1, MILLISECONDS);
verify(subchannel1a, never()).shutdown();
clock.forwardTime(1, MILLISECONDS);
@ -182,6 +204,51 @@ public class CachedSubchannelPoolTest {
assertThat(clock.numPendingTasks()).isEqualTo(0);
}
@Test
public void updateStateWhileInPool() {
Subchannel subchannel1 = pool.takeOrCreateSubchannel(EAG1, ATTRS1);
Subchannel subchannel2 = pool.takeOrCreateSubchannel(EAG2, ATTRS2);
pool.returnSubchannel(subchannel1, READY_STATE);
pool.returnSubchannel(subchannel2, TRANSIENT_FAILURE_STATE);
ConnectivityStateInfo anotherFailureState =
ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("Another"));
pool.handleSubchannelState(subchannel1, anotherFailureState);
verify(balancer, never())
.handleSubchannelState(any(Subchannel.class), any(ConnectivityStateInfo.class));
assertThat(pool.takeOrCreateSubchannel(EAG1, ATTRS1)).isSameAs(subchannel1);
verify(balancer).handleSubchannelState(same(subchannel1), same(anotherFailureState));
verifyNoMoreInteractions(balancer);
assertThat(pool.takeOrCreateSubchannel(EAG2, ATTRS2)).isSameAs(subchannel2);
verify(balancer).handleSubchannelState(same(subchannel2), same(TRANSIENT_FAILURE_STATE));
verifyNoMoreInteractions(balancer);
}
@Test
public void updateStateWhileInPool_notSameObject() {
Subchannel subchannel1 = pool.takeOrCreateSubchannel(EAG1, ATTRS1);
pool.returnSubchannel(subchannel1, READY_STATE);
Subchannel subchannel2 = helper.createSubchannel(EAG1, ATTRS1);
Subchannel subchannel3 = helper.createSubchannel(EAG2, ATTRS2);
// subchannel2 is not in the pool, although with the same address
pool.handleSubchannelState(subchannel2, TRANSIENT_FAILURE_STATE);
// subchannel3 is not in the pool. In fact its address is not in the pool
pool.handleSubchannelState(subchannel3, TRANSIENT_FAILURE_STATE);
assertThat(pool.takeOrCreateSubchannel(EAG1, ATTRS1)).isSameAs(subchannel1);
// subchannel1's state is unchanged
verify(balancer).handleSubchannelState(same(subchannel1), same(READY_STATE));
verifyNoMoreInteractions(balancer);
}
@Test
public void returnDuplicateAddressSubchannel() {
Subchannel subchannel1 = pool.takeOrCreateSubchannel(EAG1, ATTRS1);
@ -190,20 +257,20 @@ public class CachedSubchannelPoolTest {
assertThat(subchannel1).isNotSameAs(subchannel2);
assertThat(clock.getPendingTasks(SHUTDOWN_TASK_FILTER)).isEmpty();
pool.returnSubchannel(subchannel2);
pool.returnSubchannel(subchannel2, READY_STATE);
assertThat(clock.getPendingTasks(SHUTDOWN_TASK_FILTER)).hasSize(1);
// If the subchannel being returned has an address that is the same as a subchannel in the pool,
// the returned subchannel will be shut down.
verify(subchannel1, never()).shutdown();
pool.returnSubchannel(subchannel1);
pool.returnSubchannel(subchannel1, READY_STATE);
assertThat(clock.getPendingTasks(SHUTDOWN_TASK_FILTER)).hasSize(1);
verify(subchannel1).shutdown();
pool.returnSubchannel(subchannel3);
pool.returnSubchannel(subchannel3, READY_STATE);
assertThat(clock.getPendingTasks(SHUTDOWN_TASK_FILTER)).hasSize(2);
// Returning the same subchannel twice has no effect.
pool.returnSubchannel(subchannel3);
pool.returnSubchannel(subchannel3, READY_STATE);
assertThat(clock.getPendingTasks(SHUTDOWN_TASK_FILTER)).hasSize(2);
verify(subchannel2, never()).shutdown();
@ -216,8 +283,8 @@ public class CachedSubchannelPoolTest {
Subchannel subchannel2 = pool.takeOrCreateSubchannel(EAG2, ATTRS2);
Subchannel subchannel3 = pool.takeOrCreateSubchannel(EAG2, ATTRS2);
pool.returnSubchannel(subchannel1);
pool.returnSubchannel(subchannel2);
pool.returnSubchannel(subchannel1, READY_STATE);
pool.returnSubchannel(subchannel2, READY_STATE);
verify(subchannel1, never()).shutdown();
verify(subchannel2, never()).shutdown();

View File

@ -291,7 +291,7 @@ public class GrpclbLoadBalancerTest {
when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2);
balancer = new GrpclbLoadBalancer(helper, subchannelPool, fakeClock.getTimeProvider(),
backoffPolicyProvider);
verify(subchannelPool).init(same(helper));
verify(subchannelPool).init(same(helper), same(balancer));
}
@After
@ -313,7 +313,7 @@ public class GrpclbLoadBalancerTest {
}
// GRPCLB manages subchannels only through subchannelPool
for (Subchannel subchannel : pooledSubchannelTracker) {
verify(subchannelPool).returnSubchannel(same(subchannel));
verify(subchannelPool).returnSubchannel(same(subchannel), any(ConnectivityStateInfo.class));
// Our mock subchannelPool never calls Subchannel.shutdown(), thus we can tell if
// LoadBalancer has called it expectedly.
verify(subchannel, never()).shutdown();
@ -1040,8 +1040,9 @@ public class GrpclbLoadBalancerTest {
inOrder.verifyNoMoreInteractions();
// As long as there is at least one READY subchannel, round robin will work.
Status error1 = Status.UNAVAILABLE.withDescription("error1");
deliverSubchannelState(subchannel1, ConnectivityStateInfo.forTransientFailure(error1));
ConnectivityStateInfo errorState1 =
ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("error1"));
deliverSubchannelState(subchannel1, errorState1);
inOrder.verifyNoMoreInteractions();
// If no subchannel is READY, some with error and the others are IDLE, will report CONNECTING
@ -1065,7 +1066,8 @@ public class GrpclbLoadBalancerTest {
new ServerEntry("127.0.0.1", 2010, "token0004"), // Existing address with token changed
new ServerEntry("127.0.0.1", 2030, "token0005"), // New address appearing second time
new ServerEntry("token0006")); // drop
verify(subchannelPool, never()).returnSubchannel(same(subchannel1));
verify(subchannelPool, never())
.returnSubchannel(same(subchannel1), any(ConnectivityStateInfo.class));
lbResponseObserver.onNext(buildLbResponse(backends2));
assertThat(logs).containsExactly(
@ -1081,9 +1083,10 @@ public class GrpclbLoadBalancerTest {
logs.clear();
// not in backends2, closed
verify(subchannelPool).returnSubchannel(same(subchannel1));
verify(subchannelPool).returnSubchannel(same(subchannel1), same(errorState1));
// backends2[2], will be kept
verify(subchannelPool, never()).returnSubchannel(same(subchannel2));
verify(subchannelPool, never())
.returnSubchannel(same(subchannel2), any(ConnectivityStateInfo.class));
inOrder.verify(subchannelPool, never()).takeOrCreateSubchannel(
eq(new EquivalentAddressGroup(backends2.get(2).addr, LB_BACKEND_ATTRS)),
@ -1091,6 +1094,13 @@ public class GrpclbLoadBalancerTest {
inOrder.verify(subchannelPool).takeOrCreateSubchannel(
eq(new EquivalentAddressGroup(backends2.get(0).addr, LB_BACKEND_ATTRS)),
any(Attributes.class));
ConnectivityStateInfo errorOnCachedSubchannel1 =
ConnectivityStateInfo.forTransientFailure(
Status.UNAVAILABLE.withDescription("You can get this error even if you are cached"));
deliverSubchannelState(subchannel1, errorOnCachedSubchannel1);
verify(subchannelPool).handleSubchannelState(same(subchannel1), same(errorOnCachedSubchannel1));
assertEquals(1, mockSubchannels.size());
Subchannel subchannel3 = mockSubchannels.poll();
verify(subchannel3).requestConnection();
@ -1107,11 +1117,15 @@ public class GrpclbLoadBalancerTest {
new DropEntry(getLoadRecorder(), "token0006")).inOrder();
assertThat(picker7.pickList).containsExactly(BUFFER_ENTRY);
// State updates on obsolete subchannel1 will have no effect
// State updates on obsolete subchannel1 will only be passed to the pool
deliverSubchannelState(subchannel1, ConnectivityStateInfo.forNonError(READY));
deliverSubchannelState(
subchannel1, ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE));
deliverSubchannelState(subchannel1, ConnectivityStateInfo.forNonError(SHUTDOWN));
inOrder.verify(subchannelPool)
.handleSubchannelState(same(subchannel1), eq(ConnectivityStateInfo.forNonError(READY)));
inOrder.verify(subchannelPool).handleSubchannelState(
same(subchannel1), eq(ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE)));
inOrder.verifyNoMoreInteractions();
deliverSubchannelState(subchannel3, ConnectivityStateInfo.forNonError(READY));
@ -1141,12 +1155,15 @@ public class GrpclbLoadBalancerTest {
new BackendEntry(subchannel3, getLoadRecorder(), "token0003"),
new BackendEntry(subchannel2, getLoadRecorder(), "token0004"),
new BackendEntry(subchannel3, getLoadRecorder(), "token0005")).inOrder();
verify(subchannelPool, never()).returnSubchannel(same(subchannel3));
verify(subchannelPool, never())
.returnSubchannel(same(subchannel3), any(ConnectivityStateInfo.class));
// Update backends, with no entry
lbResponseObserver.onNext(buildLbResponse(Collections.<ServerEntry>emptyList()));
verify(subchannelPool).returnSubchannel(same(subchannel2));
verify(subchannelPool).returnSubchannel(same(subchannel3));
verify(subchannelPool)
.returnSubchannel(same(subchannel2), eq(ConnectivityStateInfo.forNonError(READY)));
verify(subchannelPool)
.returnSubchannel(same(subchannel3), eq(ConnectivityStateInfo.forNonError(READY)));
inOrder.verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture());
RoundRobinPicker picker10 = (RoundRobinPicker) pickerCaptor.getValue();
assertThat(picker10.dropList).isEmpty();
@ -1790,7 +1807,8 @@ public class GrpclbLoadBalancerTest {
// PICK_FIRST doesn't use subchannelPool
verify(subchannelPool, never())
.takeOrCreateSubchannel(any(EquivalentAddressGroup.class), any(Attributes.class));
verify(subchannelPool, never()).returnSubchannel(any(Subchannel.class));
verify(subchannelPool, never())
.returnSubchannel(any(Subchannel.class), any(ConnectivityStateInfo.class));
}
@SuppressWarnings("unchecked")
@ -1869,7 +1887,8 @@ public class GrpclbLoadBalancerTest {
// PICK_FIRST doesn't use subchannelPool
verify(subchannelPool, never())
.takeOrCreateSubchannel(any(EquivalentAddressGroup.class), any(Attributes.class));
verify(subchannelPool, never()).returnSubchannel(any(Subchannel.class));
verify(subchannelPool, never())
.returnSubchannel(any(Subchannel.class), any(ConnectivityStateInfo.class));
}
@Test
@ -1914,7 +1933,8 @@ public class GrpclbLoadBalancerTest {
assertEquals(2, mockSubchannels.size());
Subchannel subchannel1 = mockSubchannels.poll();
Subchannel subchannel2 = mockSubchannels.poll();
verify(subchannelPool, never()).returnSubchannel(any(Subchannel.class));
verify(subchannelPool, never())
.returnSubchannel(any(Subchannel.class), any(ConnectivityStateInfo.class));
// Switch to PICK_FIRST
lbConfig = "{\"childPolicy\" : [ {\"pick_first\" : {}} ]}";
@ -1925,8 +1945,10 @@ public class GrpclbLoadBalancerTest {
// GrpclbState will be shutdown, and a new one will be created
assertThat(oobChannel.isShutdown()).isTrue();
verify(subchannelPool).returnSubchannel(same(subchannel1));
verify(subchannelPool).returnSubchannel(same(subchannel2));
verify(subchannelPool)
.returnSubchannel(same(subchannel1), eq(ConnectivityStateInfo.forNonError(IDLE)));
verify(subchannelPool)
.returnSubchannel(same(subchannel2), eq(ConnectivityStateInfo.forNonError(IDLE)));
// A new LB stream is created
assertEquals(1, fakeOobChannels.size());