Skip to content

Commit 7adbffe

Browse files
committed
core: Only use scheduled executor for timer tasks
This removes an abuse of scheduled executor in ManagedChannelImpl. The executor was used to avoid deadlocking. Now we run the work on the same thread, but delay it until locks have been released. Fixes #2444
1 parent 3b15fa3 commit 7adbffe

6 files changed

Lines changed: 78 additions & 49 deletions

File tree

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import java.util.HashSet;
3535

36+
import javax.annotation.CheckReturnValue;
3637
import javax.annotation.concurrent.GuardedBy;
3738

3839
/**
@@ -44,15 +45,18 @@ abstract class InUseStateAggregator<T> {
4445
private final HashSet<T> inUseObjects = new HashSet<T>();
4546

4647
/**
47-
* Update the in-use state of an object. Initially no object is in use.
48+
* Update the in-use state of an object. Initially no object is in use. When the return value is
49+
* non-{@code null}, the caller should execute the runnable after releasing locks.
4850
*/
49-
final void updateObjectInUse(T object, boolean inUse) {
51+
@CheckReturnValue
52+
final Runnable updateObjectInUse(T object, boolean inUse) {
53+
Runnable runnable = null;
5054
synchronized (getLock()) {
5155
int origSize = inUseObjects.size();
5256
if (inUse) {
5357
inUseObjects.add(object);
5458
if (origSize == 0) {
55-
handleInUse();
59+
runnable = handleInUse();
5660
}
5761
} else {
5862
boolean removed = inUseObjects.remove(object);
@@ -61,8 +65,10 @@ final void updateObjectInUse(T object, boolean inUse) {
6165
}
6266
}
6367
}
68+
return runnable;
6469
}
6570

71+
@CheckReturnValue
6672
final boolean isInUse() {
6773
synchronized (getLock()) {
6874
return !inUseObjects.isEmpty();
@@ -73,12 +79,13 @@ final boolean isInUse() {
7379

7480
/**
7581
* Called when the aggregated in-use state has changed to true, which means at least one object is
76-
* in use.
82+
* in use. When the return value is non-{@code null}, then the runnable will be executed by the
83+
* caller of {@link #updateObjectInUse} after releasing locks.
7784
*
7885
* <p>This method is called under the lock returned by {@link #getLock}.
7986
*/
8087
@GuardedBy("getLock()")
81-
abstract void handleInUse();
88+
abstract Runnable handleInUse();
8289

8390
/**
8491
* Called when the aggregated in-use state has changed to false, which means no object is in use.

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

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ Object getLock() {
165165

166166
@Override
167167
@GuardedBy("lock")
168-
void handleInUse() {
169-
exitIdleMode();
168+
Runnable handleInUse() {
169+
return exitIdleMode();
170170
}
171171

172172
@GuardedBy("lock")
@@ -220,32 +220,46 @@ public void run() {
220220
/**
221221
* Make the channel exit idle mode, if it's in it. Return a LoadBalancer that can be used for
222222
* making new requests. Return null if the channel is shutdown.
223-
*
224-
* <p>May be called under the lock.
225223
*/
226224
@VisibleForTesting
227-
LoadBalancer<ClientTransport> exitIdleMode() {
225+
LoadBalancer<ClientTransport> exitIdleModeAndGetLb() {
226+
Runnable runnable;
227+
LoadBalancer<ClientTransport> balancer;
228+
synchronized (lock) {
229+
runnable = exitIdleMode();
230+
balancer = loadBalancer;
231+
}
232+
if (runnable != null) {
233+
runnable.run();
234+
}
235+
return balancer;
236+
}
237+
238+
/**
239+
* Make the channel exit idle mode, if it's in it. If the returned runnable is non-{@code null},
240+
* then it should be executed by the caller after releasing {@code lock}.
241+
*/
242+
@GuardedBy("lock")
243+
private Runnable exitIdleMode() {
228244
final LoadBalancer<ClientTransport> balancer;
229245
final NameResolver resolver;
230-
synchronized (lock) {
231-
if (shutdown) {
232-
return null;
233-
}
234-
if (inUseStateAggregator.isInUse()) {
235-
cancelIdleTimer();
236-
} else {
237-
// exitIdleMode() may be called outside of inUseStateAggregator, which may still in
238-
// "not-in-use" state. If it's the case, we start the timer which will be soon cancelled if
239-
// the aggregator receives actual uses.
240-
rescheduleIdleTimer();
241-
}
242-
if (loadBalancer != null) {
243-
return loadBalancer;
244-
}
245-
balancer = loadBalancerFactory.newLoadBalancer(nameResolver.getServiceAuthority(), tm);
246-
this.loadBalancer = balancer;
247-
resolver = this.nameResolver;
246+
if (shutdown) {
247+
return null;
248248
}
249+
if (inUseStateAggregator.isInUse()) {
250+
cancelIdleTimer();
251+
} else {
252+
// exitIdleMode() may be called outside of inUseStateAggregator, which may still in
253+
// "not-in-use" state. If it's the case, we start the timer which will be soon cancelled if
254+
// the aggregator receives actual uses.
255+
rescheduleIdleTimer();
256+
}
257+
if (loadBalancer != null) {
258+
return null;
259+
}
260+
balancer = loadBalancerFactory.newLoadBalancer(nameResolver.getServiceAuthority(), tm);
261+
this.loadBalancer = balancer;
262+
resolver = this.nameResolver;
249263
class NameResolverStartTask implements Runnable {
250264
@Override
251265
public void run() {
@@ -255,8 +269,7 @@ public void run() {
255269
}
256270
}
257271

258-
scheduledExecutor.execute(new NameResolverStartTask());
259-
return balancer;
272+
return new NameResolverStartTask();
260273
}
261274

262275
@GuardedBy("lock")
@@ -294,7 +307,7 @@ private void rescheduleIdleTimer() {
294307
private final ClientTransportProvider transportProvider = new ClientTransportProvider() {
295308
@Override
296309
public ClientTransport get(CallOptions callOptions) {
297-
LoadBalancer<ClientTransport> balancer = exitIdleMode();
310+
LoadBalancer<ClientTransport> balancer = exitIdleModeAndGetLb();
298311
if (balancer == null) {
299312
return SHUTDOWN_TRANSPORT;
300313
}
@@ -618,13 +631,14 @@ public void onConnectionClosedByServer(Status status) {
618631
}
619632

620633
@Override
621-
public void onInUse(TransportSet ts) {
622-
inUseStateAggregator.updateObjectInUse(ts, true);
634+
public Runnable onInUse(TransportSet ts) {
635+
return inUseStateAggregator.updateObjectInUse(ts, true);
623636
}
624637

625638
@Override
626639
public void onNotInUse(TransportSet ts) {
627-
inUseStateAggregator.updateObjectInUse(ts, false);
640+
Runnable r = inUseStateAggregator.updateObjectInUse(ts, false);
641+
assert r == null;
628642
}
629643
});
630644
if (log.isLoggable(Level.FINE)) {
@@ -709,13 +723,17 @@ private class InterimTransportImpl implements InterimTransport<ClientTransport>
709723
delayedTransports.remove(delayedTransport);
710724
maybeTerminateChannel();
711725
}
712-
inUseStateAggregator.updateObjectInUse(delayedTransport, false);
726+
Runnable r = inUseStateAggregator.updateObjectInUse(delayedTransport, false);
727+
assert r == null;
713728
}
714729

715730
@Override public void transportReady() {}
716731

717732
@Override public void transportInUse(boolean inUse) {
718-
inUseStateAggregator.updateObjectInUse(delayedTransport, inUse);
733+
Runnable r = inUseStateAggregator.updateObjectInUse(delayedTransport, inUse);
734+
if (r != null) {
735+
r.run();
736+
}
719737
}
720738
});
721739
boolean savedShutdown;

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ Object getLock() {
110110
}
111111

112112
@Override
113-
void handleInUse() {
114-
callback.onInUse(TransportSet.this);
113+
Runnable handleInUse() {
114+
return callback.onInUse(TransportSet.this);
115115
}
116116

117117
@Override
@@ -370,7 +370,10 @@ public void transportReady() {}
370370

371371
@Override
372372
public void transportInUse(boolean inUse) {
373-
inUseStateAggregator.updateObjectInUse(transport, inUse);
373+
Runnable r = inUseStateAggregator.updateObjectInUse(transport, inUse);
374+
if (r != null) {
375+
r.run();
376+
}
374377
}
375378

376379
@Override
@@ -379,7 +382,8 @@ public void transportShutdown(Status status) {}
379382
@Override
380383
public void transportTerminated() {
381384
boolean runCallback = false;
382-
inUseStateAggregator.updateObjectInUse(transport, false);
385+
Runnable r = inUseStateAggregator.updateObjectInUse(transport, false);
386+
assert r == null;
383387
synchronized (lock) {
384388
transports.remove(transport);
385389
if (shutdown && transports.isEmpty()) {
@@ -519,9 +523,13 @@ public void onConnectionClosedByServer(Status status) { }
519523

520524
/**
521525
* Called when the TransportSet's in-use state has changed to true, which means at least one
522-
* transport is in use. This method is called under a lock thus externally synchronized.
526+
* transport is in use. This method is called under a lock thus externally synchronized. If the
527+
* return value is non-{@code null}, the runnable will be executed after releasing the lock.
523528
*/
524-
public void onInUse(TransportSet ts) { }
529+
@CheckReturnValue
530+
public Runnable onInUse(TransportSet ts) {
531+
return null;
532+
}
525533

526534
/**
527535
* Called when the TransportSet's in-use state has changed to false, which means no transport is

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,7 @@ private void walkIntoIdleMode(Collection<MockClientTransportInfo> currentTranspo
338338
}
339339

340340
private void forceExitIdleMode() {
341-
channel.exitIdleMode();
342-
// NameResolver is started in the scheduled executor
343-
timer.runDueTasks();
341+
channel.exitIdleModeAndGetLb();
344342
}
345343

346344
private ClientTransport channelTmGetTransportUnwrapped(EquivalentAddressGroup addressGroup) {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ private void createChannel(
151151
ManagedChannelImpl.IDLE_TIMEOUT_MILLIS_DISABLE,
152152
executor.scheduledExecutorService, userAgent, interceptors);
153153
// Force-exit the initial idle-mode
154-
channel.exitIdleMode();
155-
// Will start NameResolver in the scheduled executor
156-
assertEquals(1, timer.runDueTasks());
154+
channel.exitIdleModeAndGetLb();
157155
}
158156

159157
@Before

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public void setUp() {
140140
ArgumentCaptor<TransportManager<ClientTransport>> tmCaptor
141141
= ArgumentCaptor.forClass(null);
142142
// Force Channel to exit the initial idleness to get NameResolver and LoadBalancer created.
143-
channel.exitIdleMode();
143+
channel.exitIdleModeAndGetLb();
144144
verify(mockNameResolverFactory).newNameResolver(any(URI.class), any(Attributes.class));
145145
verify(mockLoadBalancerFactory).newLoadBalancer(anyString(), tmCaptor.capture());
146146
tm = tmCaptor.getValue();

0 commit comments

Comments
 (0)