@@ -94,6 +94,27 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
9494
9595 static final long IDLE_TIMEOUT_MILLIS_DISABLE = -1 ;
9696
97+ /**
98+ * The time after idleTimeoutMillis expires before idleness takes effect. The time before
99+ * idleTimeoutMillis expires is part of a fast path for acquiring the load balancer. After
100+ * idleTimeoutMillis expires a slow path takes effect with extra synchronization.
101+ *
102+ * <p>Transports having open streams prevents entering idle mode. However, this creates an
103+ * inherent race between acquiring a transport, which can't be done in idle mode, and the RPC
104+ * actually being created on that transport, which inhibits idle mode. Thus we reset the idle
105+ * timer when acquiring a transport, and impose a minimum idle time (IDLE_MODE_MIN_TIMEOUT_MILLIS)
106+ * to make the chances of racing very small. If we do race, then the RPC will spuriously fail
107+ * because the transport chosen was shut down.
108+ *
109+ * <p>For heavy users, resetting the idle timer each RPC becomes highly contended. We instead only
110+ * need to reset the timer when it is close to expiring. We do the equivalent by having two
111+ * periods: a reduced regular idle time period and the extra time as a grace period. We ignore the
112+ * race during the regular idle time period, but any acquisition during the grace period must
113+ * reset the timer.
114+ */
115+ @ VisibleForTesting
116+ static final long IDLE_GRACE_PERIOD_MILLIS = TimeUnit .SECONDS .toMillis (1 );
117+
97118 private static final ClientTransport SHUTDOWN_TRANSPORT =
98119 new FailingClientTransport (Status .UNAVAILABLE .withDescription ("Channel is shutdown" ));
99120
@@ -115,6 +136,7 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
115136
116137 private final SharedResourceHolder .Resource <ScheduledExecutorService > timerService ;
117138 private final Supplier <Stopwatch > stopwatchSupplier ;
139+ /** The timout before entering idle mode, less {@link #IDLE_GRACE_PERIOD_MILLIS}. */
118140 private final long idleTimeoutMillis ;
119141 private final CensusContextFactory censusFactory ;
120142
@@ -135,10 +157,14 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
135157 // Never be null. Must be modified under lock.
136158 private NameResolver nameResolver ;
137159
138- // null iff channel is in idle state
160+ /** {@code null} when idle or when in grace idle period. "lock" must be held when modifying. */
161+ @ Nullable
162+ private volatile LoadBalancer <ClientTransport > loadBalancer ;
163+
164+ /** non-{code null} iff channel is in grace idle period. */
139165 @ GuardedBy ("lock" )
140166 @ Nullable
141- private LoadBalancer <ClientTransport > loadBalancer ;
167+ private LoadBalancer <ClientTransport > graceLoadBalancer ;
142168
143169 /**
144170 * Maps EquivalentAddressGroups to transports for that server. "lock" must be held when mutating.
@@ -197,9 +223,18 @@ public void run() {
197223 // Race detected: this task started before cancelIdleTimer() could cancel it.
198224 return ;
199225 }
226+ if (loadBalancer != null ) {
227+ // Enter grace period.
228+ graceLoadBalancer = loadBalancer ;
229+ loadBalancer = null ;
230+ assert idleModeTimer == this ;
231+ idleModeTimerFuture = scheduledExecutor .schedule (new LogExceptionRunnable (idleModeTimer ),
232+ IDLE_GRACE_PERIOD_MILLIS , TimeUnit .MILLISECONDS );
233+ return ;
234+ }
200235 // Enter idle mode
201- savedBalancer = loadBalancer ;
202- loadBalancer = null ;
236+ savedBalancer = graceLoadBalancer ;
237+ graceLoadBalancer = null ;
203238 oldResolver = nameResolver ;
204239 nameResolver = getNameResolver (target , nameResolverFactory , nameResolverParams );
205240 transportsCopy .addAll (transports .values ());
@@ -243,6 +278,11 @@ LoadBalancer<ClientTransport> exitIdleMode() {
243278 // the aggregator receives actual uses.
244279 rescheduleIdleTimer ();
245280 }
281+ if (graceLoadBalancer != null ) {
282+ // Exit grace period; timer already rescheduled above.
283+ loadBalancer = graceLoadBalancer ;
284+ graceLoadBalancer = null ;
285+ }
246286 if (loadBalancer != null ) {
247287 return loadBalancer ;
248288 }
@@ -268,6 +308,13 @@ public void run() {
268308 return balancer ;
269309 }
270310
311+ @ VisibleForTesting
312+ boolean isInIdleGracePeriod () {
313+ synchronized (lock ) {
314+ return graceLoadBalancer != null ;
315+ }
316+ }
317+
271318 // ErrorProne's GuardedByChecker can't figure out that the idleModeTimer is a nested instance of
272319 // this particular instance. It is worried about something like:
273320 // ManagedChannelImpl a = ...;
@@ -312,7 +359,11 @@ private void rescheduleIdleTimer() {
312359 private final ClientTransportProvider transportProvider = new ClientTransportProvider () {
313360 @ Override
314361 public ClientTransport get (CallOptions callOptions ) {
315- LoadBalancer <ClientTransport > balancer = exitIdleMode ();
362+ LoadBalancer <ClientTransport > balancer = loadBalancer ;
363+ if (balancer == null ) {
364+ // Current state is either idle or in grace period
365+ balancer = exitIdleMode ();
366+ }
316367 if (balancer == null ) {
317368 return SHUTDOWN_TRANSPORT ;
318369 }
@@ -347,9 +398,15 @@ public ClientTransport get(CallOptions callOptions) {
347398 this .timerService = timerService ;
348399 this .scheduledExecutor = SharedResourceHolder .get (timerService );
349400 this .stopwatchSupplier = checkNotNull (stopwatchSupplier , "stopwatchSupplier" );
350- checkArgument (idleTimeoutMillis > 0 || idleTimeoutMillis == IDLE_TIMEOUT_MILLIS_DISABLE ,
351- "invalid idleTimeoutMillis %s" , idleTimeoutMillis );
352- this .idleTimeoutMillis = idleTimeoutMillis ;
401+ if (idleTimeoutMillis == IDLE_TIMEOUT_MILLIS_DISABLE ) {
402+ this .idleTimeoutMillis = idleTimeoutMillis ;
403+ } else {
404+ assert IDLE_GRACE_PERIOD_MILLIS
405+ <= AbstractManagedChannelImplBuilder .IDLE_MODE_MIN_TIMEOUT_MILLIS ;
406+ checkArgument (idleTimeoutMillis >= IDLE_GRACE_PERIOD_MILLIS ,
407+ "invalid idleTimeoutMillis %s" , idleTimeoutMillis );
408+ this .idleTimeoutMillis = idleTimeoutMillis - IDLE_GRACE_PERIOD_MILLIS ;
409+ }
353410 this .decompressorRegistry = decompressorRegistry ;
354411 this .compressorRegistry = compressorRegistry ;
355412 this .userAgent = userAgent ;
@@ -435,7 +492,7 @@ public ManagedChannelImpl shutdown() {
435492 delayedTransportsCopy .addAll (delayedTransports );
436493 oobTransportsCopy .addAll (oobTransports );
437494 }
438- balancer = loadBalancer ;
495+ balancer = getCurrentLoadBalancer () ;
439496 resolver = nameResolver ;
440497 cancelIdleTimer ();
441498 }
@@ -539,6 +596,15 @@ public String authority() {
539596 return interceptorChannel .authority ();
540597 }
541598
599+ /** Returns {@code null} iff channel is in idle state. */
600+ @ GuardedBy ("lock" )
601+ private LoadBalancer <ClientTransport > getCurrentLoadBalancer () {
602+ if (loadBalancer != null ) {
603+ return loadBalancer ;
604+ }
605+ return graceLoadBalancer ;
606+ }
607+
542608 private class RealChannel extends Channel {
543609 @ Override
544610 public <ReqT , RespT > ClientCall <ReqT , RespT > newCall (MethodDescriptor <ReqT , RespT > method ,
@@ -608,12 +674,12 @@ public ClientTransport getTransport(final EquivalentAddressGroup addressGroup) {
608674 if (shutdown ) {
609675 return SHUTDOWN_TRANSPORT ;
610676 }
611- if (loadBalancer == null ) {
677+ if (getCurrentLoadBalancer () == null ) {
612678 return IDLE_MODE_TRANSPORT ;
613679 }
614680 ts = transports .get (addressGroup );
615681 if (ts == null ) {
616- ts = new TransportSet (addressGroup , authority (), userAgent , loadBalancer ,
682+ ts = new TransportSet (addressGroup , authority (), userAgent , getCurrentLoadBalancer () ,
617683 backoffPolicyProvider , transportFactory , scheduledExecutor , stopwatchSupplier ,
618684 executor , new TransportSet .Callback () {
619685 @ Override
@@ -775,14 +841,14 @@ private class OobTransportProviderImpl implements OobTransportProvider<ClientTra
775841 if (shutdown ) {
776842 transportSet = null ;
777843 transport = SHUTDOWN_TRANSPORT ;
778- } else if (loadBalancer == null ) {
844+ } else if (getCurrentLoadBalancer () == null ) {
779845 transportSet = null ;
780846 transport = IDLE_MODE_TRANSPORT ;
781847 } else {
782848 transport = null ;
783- transportSet = new TransportSet (addressGroup , authority , userAgent , loadBalancer ,
784- backoffPolicyProvider , transportFactory , scheduledExecutor , stopwatchSupplier ,
785- executor , new TransportSet .Callback () {
849+ transportSet = new TransportSet (addressGroup , authority , userAgent ,
850+ getCurrentLoadBalancer (), backoffPolicyProvider , transportFactory , scheduledExecutor ,
851+ stopwatchSupplier , executor , new TransportSet .Callback () {
786852 @ Override
787853 public void onTerminated (TransportSet ts ) {
788854 synchronized (lock ) {
0 commit comments