Skip to content

Commit cb73f21

Browse files
authored
core: Release lock before closing shared resource
If a resource has dependencies that also use SharedResourceHolder, and its close() blocks waiting for processing on another thread, then the threads could become deadlocked on the SharedResourceHolder lock. Our answer should be "don't block," but it's also good to avoid calling arbitrary code with a lock held. create() is still called with the lock held, but that seems less likely to do work on another thread, and it is harder to avoid the lock. close() is very easy to call without the lock. See d50098f and b/458736211
1 parent e2d5bad commit cb73f21

File tree

2 files changed

+50
-10
lines changed

2 files changed

+50
-10
lines changed

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,16 @@ synchronized <T> T releaseInternal(final Resource<T> resource, final T instance)
134134
public void run() {
135135
synchronized (SharedResourceHolder.this) {
136136
// Refcount may have gone up since the task was scheduled. Re-check it.
137-
if (cached.refcount == 0) {
138-
try {
139-
resource.close(instance);
140-
} finally {
141-
instances.remove(resource);
142-
if (instances.isEmpty()) {
143-
destroyer.shutdown();
144-
destroyer = null;
145-
}
146-
}
137+
if (cached.refcount != 0) {
138+
return;
139+
}
140+
instances.remove(resource);
141+
if (instances.isEmpty()) {
142+
destroyer.shutdown();
143+
destroyer = null;
147144
}
148145
}
146+
resource.close(instance);
149147
}
150148
}), DESTROY_DELAY_SECONDS, TimeUnit.SECONDS);
151149
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030

3131
import io.grpc.internal.SharedResourceHolder.Resource;
3232
import java.util.LinkedList;
33+
import java.util.concurrent.CyclicBarrier;
3334
import java.util.concurrent.Delayed;
35+
import java.util.concurrent.FutureTask;
3436
import java.util.concurrent.ScheduledExecutorService;
3537
import java.util.concurrent.ScheduledFuture;
3638
import java.util.concurrent.TimeUnit;
@@ -201,6 +203,46 @@ public void close(ResourceInstance instance) {
201203
assertNotSame(instance, holder.getInternal(resource));
202204
}
203205

206+
@Test(timeout = 5000)
207+
public void closeRunsConcurrently() throws Exception {
208+
CyclicBarrier barrier = new CyclicBarrier(2);
209+
class SlowResource implements Resource<ResourceInstance> {
210+
@Override
211+
public ResourceInstance create() {
212+
return new ResourceInstance();
213+
}
214+
215+
@Override
216+
public void close(ResourceInstance instance) {
217+
instance.closed = true;
218+
try {
219+
barrier.await();
220+
barrier.await();
221+
} catch (Exception ex) {
222+
throw new AssertionError(ex);
223+
}
224+
}
225+
}
226+
227+
Resource<ResourceInstance> resource = new SlowResource();
228+
ResourceInstance instance = holder.getInternal(resource);
229+
holder.releaseInternal(resource, instance);
230+
MockScheduledFuture<?> scheduledDestroyTask = scheduledDestroyTasks.poll();
231+
FutureTask<Void> runTask = new FutureTask<>(scheduledDestroyTask::runTask, null);
232+
Thread t = new Thread(runTask);
233+
t.start();
234+
235+
barrier.await(); // Ensure the other thread has blocked
236+
assertTrue(instance.closed);
237+
instance = holder.getInternal(resource);
238+
assertFalse(instance.closed);
239+
holder.releaseInternal(resource, instance);
240+
241+
barrier.await(); // Resume the other thread
242+
t.join();
243+
runTask.get(); // Check for exception
244+
}
245+
204246
private class MockExecutorFactory implements
205247
SharedResourceHolder.ScheduledExecutorFactory {
206248
@Override

0 commit comments

Comments
 (0)