From 6ee6eae5a0a38532efc26e3b45cf741a3e0049d9 Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 9 Feb 2018 11:05:09 -0800 Subject: [PATCH] core: add prepareToLoseNetwork() method to ManagedChannel --- .../src/main/java/io/grpc/ManagedChannel.java | 20 +++++++- .../io/grpc/internal/ManagedChannelImpl.java | 48 ++++++++++++++----- .../grpc/internal/ManagedChannelImplTest.java | 25 ++++++++++ 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/io/grpc/ManagedChannel.java b/core/src/main/java/io/grpc/ManagedChannel.java index d60d889bb1..286b7dfa7a 100644 --- a/core/src/main/java/io/grpc/ManagedChannel.java +++ b/core/src/main/java/io/grpc/ManagedChannel.java @@ -119,6 +119,24 @@ public abstract class ManagedChannel extends Channel { * * @since 1.8.0 */ - @ExperimentalApi + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056") public void resetConnectBackoff() {} + + /** + * Invoking this method moves the channel into the IDLE state and triggers tear-down of the + * channel's name resolver and load balancer, while still allowing on-going RPCs on the channel to + * continue. New RPCs on the channel will trigger creation of a new connection. + * + *

This is primarily intended for Android users when a device is transitioning from a cellular + * to a wifi connection. Initially the device will maintain both the cellular and wifi + * connections, but the OS also issues a notification that after a short time the cellular + * connection will be terminated. Apps may invoke this method to ensure that new RPCs are created + * using the wifi connection, rather than the soon-to-be-disconnected cellular network. + * + *

No-op if not supported by implementation. + * + * @since 1.11.0 + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056") + public void prepareToLoseNetwork() {} } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 62057732de..3d3bdf3c63 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -310,19 +310,7 @@ public final class ManagedChannelImpl extends ManagedChannel implements Instrume // could cancel the timer. return; } - logger.log(Level.FINE, "[{0}] Entering idle mode", getLogId()); - // nameResolver and loadBalancer are guaranteed to be non-null. If any of them were null, - // either the idleModeTimer ran twice without exiting the idle mode, or the task in shutdown() - // did not cancel idleModeTimer, both of which are bugs. - nameResolver.shutdown(); - nameResolverStarted = false; - nameResolver = getNameResolver(target, nameResolverFactory, nameResolverParams); - lbHelper.lb.shutdown(); - lbHelper = null; - subchannelPicker = null; - if (!channelStateManager.isDisabled()) { - channelStateManager.gotoState(IDLE); - } + enterIdleMode(); } } @@ -368,6 +356,24 @@ public final class ManagedChannelImpl extends ManagedChannel implements Instrume } } + // Must be run from channelExecutor + private void enterIdleMode() { + logger.log(Level.FINE, "[{0}] Entering idle mode", getLogId()); + // nameResolver and loadBalancer are guaranteed to be non-null. If any of them were null, + // either the idleModeTimer ran twice without exiting the idle mode, or the task in shutdown() + // did not cancel idleModeTimer, or prepareToLoseNetwork() ran while shutdown or in idle, all of + // which are bugs. + nameResolver.shutdown(); + nameResolverStarted = false; + nameResolver = getNameResolver(target, nameResolverFactory, nameResolverParams); + lbHelper.lb.shutdown(); + lbHelper = null; + subchannelPicker = null; + if (!channelStateManager.isDisabled()) { + channelStateManager.gotoState(IDLE); + } + } + // Must be run from channelExecutor private void cancelIdleTimer() { if (idleModeTimerFuture != null) { @@ -760,6 +766,22 @@ public final class ManagedChannelImpl extends ManagedChannel implements Instrume }).drain(); } + @Override + public void prepareToLoseNetwork() { + class PrepareToLoseNetworkRunnable implements Runnable { + @Override + public void run() { + if (shutdown.get() || lbHelper == null) { + return; + } + cancelIdleTimer(); + enterIdleMode(); + } + } + + channelExecutor.executeLater(new PrepareToLoseNetworkRunnable()).drain(); + } + /** * A registry that prevents channel shutdown from killing existing retry attempts that are in * backoff. diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 226a12a51a..2edb025abd 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -1507,6 +1507,31 @@ public class ManagedChannelImplTest { assertEquals(CONNECTING, channel.getState(false)); } + @Test + public void prepareToLoseNetworkEntersIdle() { + createChannel(new FakeNameResolverFactory(true), NO_INTERCEPTOR); + helper.updateBalancingState(READY, mockPicker); + assertEquals(READY, channel.getState(false)); + + channel.prepareToLoseNetwork(); + + assertEquals(IDLE, channel.getState(false)); + } + + @Test + public void prepareToLoseNetworkAfterIdleTimerIsNoOp() { + long idleTimeoutMillis = 2000L; + createChannel( + new FakeNameResolverFactory(true), NO_INTERCEPTOR, true /* request connection*/, + idleTimeoutMillis); + timer.forwardNanos(TimeUnit.MILLISECONDS.toNanos(idleTimeoutMillis)); + assertEquals(IDLE, channel.getState(false)); + + channel.prepareToLoseNetwork(); + + assertEquals(IDLE, channel.getState(false)); + } + @Test public void updateBalancingStateDoesUpdatePicker() { ClientStream mockStream = mock(ClientStream.class);