mirror of https://github.com/grpc/grpc-java.git
core: add resetConnectBackoff() method to ManagedChannel
This commit is contained in:
parent
34555e497a
commit
b31db3cc9b
|
|
@ -104,4 +104,20 @@ public abstract class ManagedChannel extends Channel {
|
||||||
public void notifyWhenStateChanged(ConnectivityState source, Runnable callback) {
|
public void notifyWhenStateChanged(ConnectivityState source, Runnable callback) {
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For subchannels that are in TRANSIENT_FAILURE state, short-circuit the backoff timer and make
|
||||||
|
* them reconnect immediately. May also attempt to invoke {@link NameResolver#refresh}.
|
||||||
|
*
|
||||||
|
* <p>This is primarily intended for Android users, where the network may experience frequent
|
||||||
|
* temporary drops. Rather than waiting for gRPC's name resolution and reconnect timers to elapse
|
||||||
|
* before reconnecting, the app may use this method as a mechanism to notify gRPC that the network
|
||||||
|
* is now available and a reconnection attempt may occur immediately.
|
||||||
|
*
|
||||||
|
* <p>No-op if not supported by the implementation.
|
||||||
|
*
|
||||||
|
* @since 1.8.0
|
||||||
|
*/
|
||||||
|
@ExperimentalApi
|
||||||
|
public void resetConnectBackoff() {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import static io.grpc.ConnectivityState.CONNECTING;
|
||||||
import static io.grpc.ConnectivityState.IDLE;
|
import static io.grpc.ConnectivityState.IDLE;
|
||||||
import static io.grpc.ConnectivityState.READY;
|
import static io.grpc.ConnectivityState.READY;
|
||||||
import static io.grpc.ConnectivityState.SHUTDOWN;
|
import static io.grpc.ConnectivityState.SHUTDOWN;
|
||||||
|
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
@ -82,7 +83,8 @@ final class InternalSubchannel implements WithLogId {
|
||||||
private int addressIndex;
|
private int addressIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The policy to control back off between reconnects. Non-{@code null} when last connect failed.
|
* The policy to control back off between reconnects. Non-{@code null} when a reconnect task is
|
||||||
|
* scheduled.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private BackoffPolicy reconnectPolicy;
|
private BackoffPolicy reconnectPolicy;
|
||||||
|
|
@ -97,6 +99,9 @@ final class InternalSubchannel implements WithLogId {
|
||||||
@Nullable
|
@Nullable
|
||||||
private ScheduledFuture<?> reconnectTask;
|
private ScheduledFuture<?> reconnectTask;
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private boolean reconnectCanceled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All transports that are not terminated. At the very least the value of {@link #activeTransport}
|
* All transports that are not terminated. At the very least the value of {@link #activeTransport}
|
||||||
* will be present, but previously used transports that still have streams or are stopping may
|
* will be present, but previously used transports that still have streams or are stopping may
|
||||||
|
|
@ -227,9 +232,9 @@ final class InternalSubchannel implements WithLogId {
|
||||||
try {
|
try {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
reconnectTask = null;
|
reconnectTask = null;
|
||||||
if (state.getState() == SHUTDOWN) {
|
if (reconnectCanceled) {
|
||||||
// Even though shutdown() will cancel this task, the task may have already started
|
// Even though cancelReconnectTask() will cancel this task, the task may have already
|
||||||
// when it's being cancelled.
|
// started when it's being canceled.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gotoNonErrorState(CONNECTING);
|
gotoNonErrorState(CONNECTING);
|
||||||
|
|
@ -253,12 +258,32 @@ final class InternalSubchannel implements WithLogId {
|
||||||
log.log(Level.FINE, "[{0}] Scheduling backoff for {1} ns", new Object[]{logId, delayNanos});
|
log.log(Level.FINE, "[{0}] Scheduling backoff for {1} ns", new Object[]{logId, delayNanos});
|
||||||
}
|
}
|
||||||
Preconditions.checkState(reconnectTask == null, "previous reconnectTask is not done");
|
Preconditions.checkState(reconnectTask == null, "previous reconnectTask is not done");
|
||||||
|
reconnectCanceled = false;
|
||||||
reconnectTask = scheduledExecutor.schedule(
|
reconnectTask = scheduledExecutor.schedule(
|
||||||
new LogExceptionRunnable(new EndOfCurrentBackoff()),
|
new LogExceptionRunnable(new EndOfCurrentBackoff()),
|
||||||
delayNanos,
|
delayNanos,
|
||||||
TimeUnit.NANOSECONDS);
|
TimeUnit.NANOSECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately attempt to reconnect if the current state is TRANSIENT_FAILURE. Otherwise this
|
||||||
|
* method has no effect.
|
||||||
|
*/
|
||||||
|
void resetConnectBackoff() {
|
||||||
|
try {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (state.getState() != TRANSIENT_FAILURE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelReconnectTask();
|
||||||
|
gotoNonErrorState(CONNECTING);
|
||||||
|
startNewTransport();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
channelExecutor.drain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private void gotoNonErrorState(ConnectivityState newState) {
|
private void gotoNonErrorState(ConnectivityState newState) {
|
||||||
gotoState(ConnectivityStateInfo.forNonError(newState));
|
gotoState(ConnectivityStateInfo.forNonError(newState));
|
||||||
|
|
@ -400,7 +425,9 @@ final class InternalSubchannel implements WithLogId {
|
||||||
private void cancelReconnectTask() {
|
private void cancelReconnectTask() {
|
||||||
if (reconnectTask != null) {
|
if (reconnectTask != null) {
|
||||||
reconnectTask.cancel(false);
|
reconnectTask.cancel(false);
|
||||||
|
reconnectCanceled = true;
|
||||||
reconnectTask = null;
|
reconnectTask = null;
|
||||||
|
reconnectPolicy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,9 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
|
||||||
|
|
||||||
private final ProxyDetector proxyDetector;
|
private final ProxyDetector proxyDetector;
|
||||||
|
|
||||||
|
// Must be accessed from the channelExecutor.
|
||||||
|
private boolean nameResolverStarted;
|
||||||
|
|
||||||
// null when channel is in idle mode. Must be assigned from channelExecutor.
|
// null when channel is in idle mode. Must be assigned from channelExecutor.
|
||||||
@Nullable
|
@Nullable
|
||||||
private LbHelperImpl lbHelper;
|
private LbHelperImpl lbHelper;
|
||||||
|
|
@ -210,6 +213,7 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
|
||||||
if (nameResolver != null) {
|
if (nameResolver != null) {
|
||||||
nameResolver.shutdown();
|
nameResolver.shutdown();
|
||||||
nameResolver = null;
|
nameResolver = null;
|
||||||
|
nameResolverStarted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Until LoadBalancer is shutdown, it may still create new subchannels. We catch them
|
// Until LoadBalancer is shutdown, it may still create new subchannels. We catch them
|
||||||
|
|
@ -266,6 +270,7 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
|
||||||
// either the idleModeTimer ran twice without exiting the idle mode, or the task in shutdown()
|
// either the idleModeTimer ran twice without exiting the idle mode, or the task in shutdown()
|
||||||
// did not cancel idleModeTimer, both of which are bugs.
|
// did not cancel idleModeTimer, both of which are bugs.
|
||||||
nameResolver.shutdown();
|
nameResolver.shutdown();
|
||||||
|
nameResolverStarted = false;
|
||||||
nameResolver = getNameResolver(target, nameResolverFactory, nameResolverParams);
|
nameResolver = getNameResolver(target, nameResolverFactory, nameResolverParams);
|
||||||
lbHelper.lb.shutdown();
|
lbHelper.lb.shutdown();
|
||||||
lbHelper = null;
|
lbHelper = null;
|
||||||
|
|
@ -312,6 +317,7 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
|
||||||
NameResolverListenerImpl listener = new NameResolverListenerImpl(lbHelper);
|
NameResolverListenerImpl listener = new NameResolverListenerImpl(lbHelper);
|
||||||
try {
|
try {
|
||||||
nameResolver.start(listener);
|
nameResolver.start(listener);
|
||||||
|
nameResolverStarted = true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
listener.onError(Status.fromThrowable(t));
|
listener.onError(Status.fromThrowable(t));
|
||||||
}
|
}
|
||||||
|
|
@ -631,6 +637,28 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
|
||||||
}).drain();
|
}).drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetConnectBackoff() {
|
||||||
|
channelExecutor.executeLater(
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (shutdown.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nameResolverStarted) {
|
||||||
|
nameResolver.refresh();
|
||||||
|
}
|
||||||
|
for (InternalSubchannel subchannel : subchannels) {
|
||||||
|
subchannel.resetConnectBackoff();
|
||||||
|
}
|
||||||
|
for (InternalSubchannel oobChannel : oobChannels) {
|
||||||
|
oobChannel.resetConnectBackoff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).drain();
|
||||||
|
}
|
||||||
|
|
||||||
private class LbHelperImpl extends LoadBalancer.Helper {
|
private class LbHelperImpl extends LoadBalancer.Helper {
|
||||||
LoadBalancer lb;
|
LoadBalancer lb;
|
||||||
final NameResolver nr;
|
final NameResolver nr;
|
||||||
|
|
|
||||||
|
|
@ -884,6 +884,64 @@ public class InternalSubchannelTest {
|
||||||
eq(addr1), eq(AUTHORITY), eq(USER_AGENT), eq(proxy));
|
eq(addr1), eq(AUTHORITY), eq(USER_AGENT), eq(proxy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetConnectBackoff() throws Exception {
|
||||||
|
SocketAddress addr = mock(SocketAddress.class);
|
||||||
|
createInternalSubchannel(addr);
|
||||||
|
|
||||||
|
// Move into TRANSIENT_FAILURE to schedule reconnect
|
||||||
|
internalSubchannel.obtainActiveTransport();
|
||||||
|
assertExactCallbackInvokes("onStateChange:CONNECTING");
|
||||||
|
verify(mockTransportFactory).newClientTransport(addr, AUTHORITY, USER_AGENT, NO_PROXY);
|
||||||
|
transports.poll().listener.transportShutdown(Status.UNAVAILABLE);
|
||||||
|
assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE);
|
||||||
|
|
||||||
|
// Save the reconnectTask
|
||||||
|
FakeClock.ScheduledTask reconnectTask = null;
|
||||||
|
for (FakeClock.ScheduledTask task : fakeClock.getPendingTasks()) {
|
||||||
|
if (task.command.toString().contains("EndOfCurrentBackoff")) {
|
||||||
|
assertNull("There shouldn't be more than one reconnectTask", reconnectTask);
|
||||||
|
assertFalse(task.isDone());
|
||||||
|
reconnectTask = task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNotNull("There should be at least one reconnectTask", reconnectTask);
|
||||||
|
|
||||||
|
internalSubchannel.resetConnectBackoff();
|
||||||
|
|
||||||
|
verify(mockTransportFactory, times(2))
|
||||||
|
.newClientTransport(addr, AUTHORITY, USER_AGENT, NO_PROXY);
|
||||||
|
assertExactCallbackInvokes("onStateChange:CONNECTING");
|
||||||
|
assertTrue(reconnectTask.isCancelled());
|
||||||
|
|
||||||
|
// Simulate a race between cancel and the task scheduler. Should be a no-op.
|
||||||
|
reconnectTask.command.run();
|
||||||
|
assertNoCallbackInvoke();
|
||||||
|
verify(mockTransportFactory, times(2))
|
||||||
|
.newClientTransport(addr, AUTHORITY, USER_AGENT, NO_PROXY);
|
||||||
|
verify(mockBackoffPolicyProvider, times(1)).get();
|
||||||
|
|
||||||
|
// Fail the reconnect attempt to verify that a fresh reconnect policy is generated after
|
||||||
|
// invoking resetConnectBackoff()
|
||||||
|
transports.poll().listener.transportShutdown(Status.UNAVAILABLE);
|
||||||
|
assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE);
|
||||||
|
verify(mockBackoffPolicyProvider, times(2)).get();
|
||||||
|
fakeClock.forwardNanos(10);
|
||||||
|
assertExactCallbackInvokes("onStateChange:CONNECTING");
|
||||||
|
assertEquals(CONNECTING, internalSubchannel.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetConnectBackoff_noopOnIdleTransport() throws Exception {
|
||||||
|
SocketAddress addr = mock(SocketAddress.class);
|
||||||
|
createInternalSubchannel(addr);
|
||||||
|
assertEquals(IDLE, internalSubchannel.getState());
|
||||||
|
|
||||||
|
internalSubchannel.resetConnectBackoff();
|
||||||
|
|
||||||
|
assertNoCallbackInvoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void createInternalSubchannel(SocketAddress ... addrs) {
|
private void createInternalSubchannel(SocketAddress ... addrs) {
|
||||||
createInternalSubChannelWithProxy(ProxyDetector.NOOP_INSTANCE, addrs);
|
createInternalSubChannelWithProxy(ProxyDetector.NOOP_INSTANCE, addrs);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1540,6 +1540,43 @@ public class ManagedChannelImplTest {
|
||||||
verify(onStateChanged, never()).run();
|
verify(onStateChanged, never()).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetConnectBackoff_refreshesNameResolver() {
|
||||||
|
FakeNameResolverFactory nameResolverFactory = new FakeNameResolverFactory(true);
|
||||||
|
createChannel(nameResolverFactory, NO_INTERCEPTOR);
|
||||||
|
FakeNameResolverFactory.FakeNameResolver nameResolver = nameResolverFactory.resolvers.get(0);
|
||||||
|
assertEquals(0, nameResolver.refreshCalled);
|
||||||
|
|
||||||
|
channel.resetConnectBackoff();
|
||||||
|
|
||||||
|
assertEquals(1, nameResolver.refreshCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetConnectBackoff_noOpWhenChannelShutdown() {
|
||||||
|
FakeNameResolverFactory nameResolverFactory = new FakeNameResolverFactory(true);
|
||||||
|
createChannel(nameResolverFactory, NO_INTERCEPTOR);
|
||||||
|
|
||||||
|
channel.shutdown();
|
||||||
|
assertTrue(channel.isShutdown());
|
||||||
|
channel.resetConnectBackoff();
|
||||||
|
|
||||||
|
FakeNameResolverFactory.FakeNameResolver nameResolver = nameResolverFactory.resolvers.get(0);
|
||||||
|
assertEquals(0, nameResolver.refreshCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetConnectBackoff_noOpWhenNameResolverNotStarted() {
|
||||||
|
FakeNameResolverFactory nameResolverFactory = new FakeNameResolverFactory(true);
|
||||||
|
createChannel(nameResolverFactory, NO_INTERCEPTOR, false /* requestConnection */,
|
||||||
|
ManagedChannelImpl.IDLE_TIMEOUT_MILLIS_DISABLE);
|
||||||
|
|
||||||
|
channel.resetConnectBackoff();
|
||||||
|
|
||||||
|
FakeNameResolverFactory.FakeNameResolver nameResolver = nameResolverFactory.resolvers.get(0);
|
||||||
|
assertEquals(0, nameResolver.refreshCalled);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void orphanedChannelsAreLogged() throws Exception {
|
public void orphanedChannelsAreLogged() throws Exception {
|
||||||
int remaining = unterminatedChannels;
|
int remaining = unterminatedChannels;
|
||||||
|
|
@ -1679,6 +1716,7 @@ public class ManagedChannelImplTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void refresh() {
|
@Override public void refresh() {
|
||||||
|
assertNotNull(listener);
|
||||||
refreshCalled++;
|
refreshCalled++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue