Skip to content

Commit 496a621

Browse files
committed
core: Use grace period to avoid exitIdleMode in fast path
Fixes #2119
1 parent 33d99ce commit 496a621

File tree

2 files changed

+141
-15
lines changed

2 files changed

+141
-15
lines changed

core/src/main/java/io/grpc/internal/ManagedChannelImpl.java

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {

core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static org.mockito.Matchers.same;
4242
import static org.mockito.Mockito.doAnswer;
4343
import static org.mockito.Mockito.never;
44+
import static org.mockito.Mockito.times;
4445
import static org.mockito.Mockito.verify;
4546
import static org.mockito.Mockito.when;
4647

@@ -194,6 +195,65 @@ public ClientTransport answer(InvocationOnMock invocation) throws Throwable {
194195
verify(mockLoadBalancer).handleResolvedAddresses(servers, Attributes.EMPTY);
195196
}
196197

198+
@Test
199+
public void newCallResetsGracePeriod() throws Exception {
200+
final EquivalentAddressGroup addressGroup = addressGroupList.get(1);
201+
doAnswer(new Answer<ClientTransport>() {
202+
@Override
203+
public ClientTransport answer(InvocationOnMock invocation) throws Throwable {
204+
return channel.tm.getTransport(addressGroup);
205+
}
206+
}).when(mockLoadBalancer).pickTransport(any(Attributes.class));
207+
208+
ClientCall<String, Integer> call = channel.newCall(method, CallOptions.DEFAULT);
209+
call.start(mockCallListener, new Metadata());
210+
call.cancel("cleanup", null);
211+
executor.runDueTasks();
212+
213+
timer.runDueTasks();
214+
verify(mockLoadBalancerFactory).newLoadBalancer(anyString(), same(channel.tm));
215+
verify(mockLoadBalancer).pickTransport(any(Attributes.class));
216+
217+
// Enter grace period
218+
timer.forwardTime(TimeUnit.SECONDS.toMillis(IDLE_TIMEOUT_SECONDS)
219+
- ManagedChannelImpl.IDLE_GRACE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
220+
assertTrue(channel.isInIdleGracePeriod());
221+
222+
call = channel.newCall(method, CallOptions.DEFAULT);
223+
call.start(mockCallListener, new Metadata());
224+
assertFalse(channel.isInIdleGracePeriod());
225+
call.cancel("cleanup", null);
226+
executor.runDueTasks();
227+
228+
// Load balancer was reused.
229+
timer.runDueTasks();
230+
verify(mockLoadBalancerFactory).newLoadBalancer(anyString(), same(channel.tm));
231+
verify(mockLoadBalancer, times(2)).pickTransport(any(Attributes.class));
232+
233+
// Now just let time pass to allow the original idle time to be well past expired.
234+
timer.forwardTime(IDLE_TIMEOUT_SECONDS - 1, TimeUnit.SECONDS);
235+
236+
call = channel.newCall(method, CallOptions.DEFAULT);
237+
call.start(mockCallListener, new Metadata());
238+
239+
// Load balancer was reused; the idle time period must have been reset.
240+
timer.runDueTasks();
241+
verify(mockLoadBalancerFactory).newLoadBalancer(anyString(), same(channel.tm));
242+
verify(mockLoadBalancer, times(3)).pickTransport(any(Attributes.class));
243+
}
244+
245+
@Test
246+
public void shutdownDuringGracePeriodShutdownLb() throws Exception {
247+
forceExitIdleMode();
248+
verify(mockLoadBalancerFactory).newLoadBalancer(anyString(), same(channel.tm));
249+
// Enter grace period
250+
timer.forwardTime(TimeUnit.SECONDS.toMillis(IDLE_TIMEOUT_SECONDS)
251+
- ManagedChannelImpl.IDLE_GRACE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
252+
verify(mockLoadBalancer, never()).shutdown();
253+
channel.shutdown();
254+
verify(mockLoadBalancer).shutdown();
255+
}
256+
197257
@Test
198258
public void enterIdleModeAfterForceExit() throws Exception {
199259
forceExitIdleMode();

0 commit comments

Comments
 (0)