Skip to content

Commit 8410194

Browse files
java-team-github-botGoogle Java Core Libraries
authored andcommitted
Fix ExecutionSequencer queue stall when delegate executor throws RejectedExecutionException.
When a delegate `Executor` throws a `RejectedExecutionException` during task execution or queue draining, `ExecutionSequencer` fails to catch the exception, leaving `taskFuture` and `newFuture` permanently uncompleted. As a result, the `ExecutionSequencer` queue permanently stalls, causing all subsequently submitted tasks to leak in memory. This change handles exceptions thrown by delegate executors in both non-reentrant and reentrant queue draining paths, cancelling unexecuted task futures to allow downstream tasks to execute. RELNOTES=`util.concurrent`: Fixed `ExecutionSequencer` queue stall when delegate executor throws `RejectedExecutionException`. PiperOrigin-RevId: 962615495
1 parent f232c38 commit 8410194

5 files changed

Lines changed: 226 additions & 22 deletions

File tree

‎android/guava-tests/test/com/google/common/util/concurrent/ExecutionSequencerTest.java‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static java.util.concurrent.Executors.newCachedThreadPool;
2323
import static java.util.concurrent.Executors.newFixedThreadPool;
2424
import static java.util.concurrent.TimeUnit.SECONDS;
25+
import static org.junit.Assert.assertThrows;
2526

2627
import com.google.common.annotations.GwtIncompatible;
2728
import com.google.common.annotations.J2ktIncompatible;
@@ -32,9 +33,12 @@
3233
import java.util.List;
3334
import java.util.concurrent.Callable;
3435
import java.util.concurrent.CountDownLatch;
36+
import java.util.concurrent.ExecutionException;
3537
import java.util.concurrent.Executor;
3638
import java.util.concurrent.ExecutorService;
3739
import java.util.concurrent.Future;
40+
import java.util.concurrent.RejectedExecutionException;
41+
import java.util.concurrent.atomic.AtomicReference;
3842
import java.util.logging.Logger;
3943
import junit.framework.TestCase;
4044
import org.jspecify.annotations.NullUnmarked;
@@ -296,6 +300,80 @@ public void testAvoidsStackOverflow_multipleThreads() throws Exception {
296300
}
297301
}
298302

303+
/*
304+
* Verifies that when a non-reentrant delegate executor rejects a task (e.g., throwing a
305+
* RejectedExecutionException because a bounded queue is full), ExecutionSequencer catches the
306+
* failure and unblocks the queue, preventing subsequent tasks from stalling or leaking.
307+
*/
308+
public void testRecoversFromNonReentrantRejectedExecution() throws Exception {
309+
// 1. Submit Task 1 (firstCallable) using directExecutor. It blocks because firstFuture is not
310+
// yet set.
311+
Future<?> unused = serializer.submitAsync(firstCallable, directExecutor());
312+
313+
Executor rejectingExecutor =
314+
task -> {
315+
throw new RejectedExecutionException();
316+
};
317+
TestCallable secondCallable = new TestCallable(immediateVoidFuture());
318+
319+
// 2. Submit Task 2 using the rejectingExecutor. When Task 1 completes, ExecutionSequencer will
320+
// attempt to dispatch Task 2 via rejectingExecutor.execute(...), which will throw
321+
// RejectedExecutionException.
322+
Future<?> failingFuture = serializer.submitAsync(secondCallable, rejectingExecutor);
323+
324+
// 3. Submit Task 3 using directExecutor. Task 3 is queued after Task 2 in ExecutionSequencer.
325+
TestCallable thirdCallable = new TestCallable(immediateVoidFuture());
326+
ListenableFuture<@Nullable Void> thirdFuture =
327+
serializer.submitAsync(thirdCallable, directExecutor());
328+
329+
// Complete Task 1's future. This triggers listener notification for Task 2.
330+
// Task 2's dispatch fails with RejectedExecutionException inside rejectingExecutor.execute().
331+
firstFuture.set(null);
332+
333+
// Verify queue recovery: Task 2's failure is handled, and Task 3 runs successfully.
334+
assertThat(secondCallable.called).isFalse();
335+
assertThrows(ExecutionException.class, () -> getDone(failingFuture));
336+
assertThat(thirdCallable.called).isTrue();
337+
assertThat(thirdFuture.isDone()).isTrue();
338+
}
339+
340+
/*
341+
* Verifies that when a reentrant delegate executor rejects a task, TaskNonReentrantExecutor.run()
342+
* handles the failure without stalling the queue or leaking subsequent submitted tasks.
343+
*/
344+
public void testRecoversFromReentrantRejectedExecution() throws Exception {
345+
Executor rejectingExecutor =
346+
task -> {
347+
throw new RejectedExecutionException();
348+
};
349+
350+
TestCallable secondCallable = new TestCallable(immediateVoidFuture());
351+
TestCallable thirdCallable = new TestCallable(immediateVoidFuture());
352+
AtomicReference<ListenableFuture<@Nullable Void>> failingFuture = new AtomicReference<>();
353+
AtomicReference<ListenableFuture<@Nullable Void>> thirdFuture = new AtomicReference<>();
354+
355+
// Submit an outer task using directExecutor.
356+
// Inside its execution, we submit Task 2 (rejectingExecutor) and Task 3 (directExecutor)
357+
// reentrantly to test reentrant queue draining error handling.
358+
Future<?> unused =
359+
serializer.submitAsync(
360+
() -> {
361+
// Reentrant submission of Task 2 to rejectingExecutor.
362+
failingFuture.set(serializer.submitAsync(secondCallable, rejectingExecutor));
363+
// Reentrant submission of Task 3 to directExecutor.
364+
thirdFuture.set(serializer.submitAsync(thirdCallable, directExecutor()));
365+
return immediateVoidFuture();
366+
},
367+
directExecutor());
368+
369+
// Verify reentrant queue recovery: Task 3 runs successfully despite Task 2's executor
370+
// rejection.
371+
assertThat(secondCallable.called).isFalse();
372+
assertThrows(ExecutionException.class, () -> getDone(failingFuture.get()));
373+
assertThat(thirdCallable.called).isTrue();
374+
assertThat(thirdFuture.get().isDone()).isTrue();
375+
}
376+
299377
@SuppressWarnings("ObjectToString") // Intended behavior
300378
public void testToString() {
301379
Future<?> unused = serializer.submitAsync(firstCallable, directExecutor());

‎android/guava/src/com/google/common/util/concurrent/ExecutionSequencer.java‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
import static com.google.common.base.Preconditions.checkNotNull;
1818
import static com.google.common.base.Preconditions.checkState;
19-
import static com.google.common.util.concurrent.ExecutionSequencer.RunningState.CANCELLED;
2019
import static com.google.common.util.concurrent.ExecutionSequencer.RunningState.NOT_RUN;
2120
import static com.google.common.util.concurrent.ExecutionSequencer.RunningState.STARTED;
21+
import static com.google.common.util.concurrent.ExecutionSequencer.RunningState.WILL_NOT_RUN;
2222
import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
2323
import static com.google.common.util.concurrent.Futures.immediateFuture;
2424
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
@@ -31,6 +31,7 @@
3131
import java.util.concurrent.Callable;
3232
import java.util.concurrent.Executor;
3333
import java.util.concurrent.Future;
34+
import java.util.concurrent.RejectedExecutionException;
3435
import java.util.concurrent.atomic.AtomicReference;
3536
import org.jspecify.annotations.Nullable;
3637

@@ -135,7 +136,7 @@ private static final class ThreadConfinedTaskQueue {
135136
@LazyInit @Nullable Thread thread;
136137

137138
/** Only used by the thread associated with this object */
138-
@Nullable Runnable nextTask;
139+
@Nullable TrustedListenableFutureTask<?> nextTask;
139140

140141
/** Only used by the thread associated with this object */
141142
@Nullable Executor nextExecutor;
@@ -267,7 +268,7 @@ public String toString() {
267268

268269
enum RunningState {
269270
NOT_RUN,
270-
CANCELLED,
271+
WILL_NOT_RUN,
271272
STARTED,
272273
}
273274

@@ -308,7 +309,7 @@ private static final class TaskNonReentrantExecutor extends AtomicReference<Runn
308309
* Set before calling delegate.execute(); set to null once run, so that it can be GCed; this
309310
* object may live on after, if submitAsync returns an incomplete future.
310311
*/
311-
@Nullable Runnable task;
312+
@Nullable TrustedListenableFutureTask<?> task;
312313

313314
/** Thread that called execute(). Set in execute, cleared when delegate.execute() returns. */
314315
@LazyInit @Nullable Thread submitting;
@@ -336,12 +337,13 @@ public void execute(Runnable task) {
336337
// from the cancelled operation and its own. This can cause one runnable to queue two tasks,
337338
// breaking the invariant this method relies on to iteratively run the next task after the
338339
// previous one completes.
339-
if (get() == RunningState.CANCELLED) {
340+
if (get() == RunningState.WILL_NOT_RUN) {
340341
delegate = null;
341342
sequencer = null;
342343
return;
343344
}
344345
submitting = Thread.currentThread();
346+
TrustedListenableFutureTask<?> trustedFutureTask = (TrustedListenableFutureTask<?>) task;
345347

346348
try {
347349
/*
@@ -361,16 +363,31 @@ public void execute(Runnable task) {
361363
// Task must be null, since each execution on this executor can only produce one more
362364
// execution.
363365
checkState(submittingTaskQueue.nextTask == null);
364-
submittingTaskQueue.nextTask = task;
366+
submittingTaskQueue.nextTask = trustedFutureTask;
365367
// requireNonNull(delegate) is safe for reasons similar to requireNonNull(sequencer).
366368
submittingTaskQueue.nextExecutor = requireNonNull(delegate);
367369
delegate = null;
368370
} else {
369371
// requireNonNull(delegate) is safe for reasons similar to requireNonNull(sequencer).
370372
Executor localDelegate = requireNonNull(delegate);
371373
delegate = null;
372-
this.task = task;
373-
localDelegate.execute(this);
374+
this.task = trustedFutureTask;
375+
try {
376+
localDelegate.execute(this);
377+
} catch (RejectedExecutionException e) {
378+
/*
379+
* `trySetCancelled` is irrelevant when we know the task won't run and when it is
380+
* harmless for our `setExceptionInternal` call here to race with a user call to
381+
* `cancel`. But we do it anyway in case we want to use the `RunningState` for another
382+
* purpose someday.
383+
*/
384+
boolean unused = trySetCancelled();
385+
trustedFutureTask.setExceptionInternal(e);
386+
/*
387+
* We don't need to clear `this.task` because no one will have a
388+
* reference to `this` after we return.
389+
*/
390+
}
374391
}
375392
} finally {
376393
// Important to null this out here - if we did *not* execute inline, we might still
@@ -437,14 +454,18 @@ public void run() {
437454
task = null;
438455
localTask.run();
439456
// Now check if our task attempted to reentrantly execute the next task.
440-
Runnable queuedTask;
457+
TrustedListenableFutureTask<?> queuedTask;
441458
Executor queuedExecutor;
442459
// Intentionally using non-short-circuit operator
443460
while ((queuedTask = executingTaskQueue.nextTask) != null
444461
&& (queuedExecutor = executingTaskQueue.nextExecutor) != null) {
445462
executingTaskQueue.nextTask = null;
446463
executingTaskQueue.nextExecutor = null;
447-
queuedExecutor.execute(queuedTask);
464+
try {
465+
queuedExecutor.execute(queuedTask);
466+
} catch (RejectedExecutionException e) {
467+
queuedTask.setExceptionInternal(e);
468+
}
448469
}
449470
} finally {
450471
// Null out the thread field, so that we don't leak a reference to Thread, and so that
@@ -462,7 +483,7 @@ private boolean trySetStarted() {
462483
}
463484

464485
private boolean trySetCancelled() {
465-
return compareAndSet(NOT_RUN, CANCELLED);
486+
return compareAndSet(NOT_RUN, WILL_NOT_RUN);
466487
}
467488
}
468489
}

‎cycle_suppress_list.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,14 @@ FIELD com.google.common.collect.TreeTraverser.PostOrderNode.childIterator
4141
FIELD com.google.common.collect.TreeTraverser.PreOrderIterator.stack
4242
FIELD com.google.common.util.concurrent.AbstractFuture.Listener.executor com.google.common.util.concurrent.MoreExecutors.rejectionPropagatingExecutor.$
4343
FIELD com.google.common.util.concurrent.AbstractService.listeners
44+
4445
# NonReentrantExecutor is not exposed to this field
4546
FIELD com.google.common.util.concurrent.ExecutionSequencer.ThreadConfinedTaskQueue.nextExecutor com.google.common.util.concurrent.ExecutionSequencer.TaskNonReentrantExecutor
47+
48+
# There is no way for a CombinedFutureInterruptibleTask to receive a TaskNonReentrantExecutor as its listenerExecutor:
49+
# A TaskNonReentrantExecutor instance is used as an Executor only in a direct call to addListener.
50+
FIELD com.google.common.util.concurrent.CombinedFuture.CombinedFutureInterruptibleTask.listenerExecutor com.google.common.util.concurrent.ExecutionSequencer.TaskNonReentrantExecutor
51+
4652
# Real cycle, but the runningState field is null'ed on completion of the future.
4753
FIELD com.google.common.util.concurrent.AggregateFuture.runningState
4854
FIELD java.util.AbstractMap.keySet com.google.common.collect.AbstractMapBasedMultimap.NavigableKeySet

‎guava-tests/test/com/google/common/util/concurrent/ExecutionSequencerTest.java‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static java.util.concurrent.Executors.newCachedThreadPool;
2323
import static java.util.concurrent.Executors.newFixedThreadPool;
2424
import static java.util.concurrent.TimeUnit.SECONDS;
25+
import static org.junit.Assert.assertThrows;
2526

2627
import com.google.common.annotations.GwtIncompatible;
2728
import com.google.common.annotations.J2ktIncompatible;
@@ -32,9 +33,12 @@
3233
import java.util.List;
3334
import java.util.concurrent.Callable;
3435
import java.util.concurrent.CountDownLatch;
36+
import java.util.concurrent.ExecutionException;
3537
import java.util.concurrent.Executor;
3638
import java.util.concurrent.ExecutorService;
3739
import java.util.concurrent.Future;
40+
import java.util.concurrent.RejectedExecutionException;
41+
import java.util.concurrent.atomic.AtomicReference;
3842
import java.util.logging.Logger;
3943
import junit.framework.TestCase;
4044
import org.jspecify.annotations.NullUnmarked;
@@ -296,6 +300,80 @@ public void testAvoidsStackOverflow_multipleThreads() throws Exception {
296300
}
297301
}
298302

303+
/*
304+
* Verifies that when a non-reentrant delegate executor rejects a task (e.g., throwing a
305+
* RejectedExecutionException because a bounded queue is full), ExecutionSequencer catches the
306+
* failure and unblocks the queue, preventing subsequent tasks from stalling or leaking.
307+
*/
308+
public void testRecoversFromNonReentrantRejectedExecution() throws Exception {
309+
// 1. Submit Task 1 (firstCallable) using directExecutor. It blocks because firstFuture is not
310+
// yet set.
311+
Future<?> unused = serializer.submitAsync(firstCallable, directExecutor());
312+
313+
Executor rejectingExecutor =
314+
task -> {
315+
throw new RejectedExecutionException();
316+
};
317+
TestCallable secondCallable = new TestCallable(immediateVoidFuture());
318+
319+
// 2. Submit Task 2 using the rejectingExecutor. When Task 1 completes, ExecutionSequencer will
320+
// attempt to dispatch Task 2 via rejectingExecutor.execute(...), which will throw
321+
// RejectedExecutionException.
322+
Future<?> failingFuture = serializer.submitAsync(secondCallable, rejectingExecutor);
323+
324+
// 3. Submit Task 3 using directExecutor. Task 3 is queued after Task 2 in ExecutionSequencer.
325+
TestCallable thirdCallable = new TestCallable(immediateVoidFuture());
326+
ListenableFuture<@Nullable Void> thirdFuture =
327+
serializer.submitAsync(thirdCallable, directExecutor());
328+
329+
// Complete Task 1's future. This triggers listener notification for Task 2.
330+
// Task 2's dispatch fails with RejectedExecutionException inside rejectingExecutor.execute().
331+
firstFuture.set(null);
332+
333+
// Verify queue recovery: Task 2's failure is handled, and Task 3 runs successfully.
334+
assertThat(secondCallable.called).isFalse();
335+
assertThrows(ExecutionException.class, () -> getDone(failingFuture));
336+
assertThat(thirdCallable.called).isTrue();
337+
assertThat(thirdFuture.isDone()).isTrue();
338+
}
339+
340+
/*
341+
* Verifies that when a reentrant delegate executor rejects a task, TaskNonReentrantExecutor.run()
342+
* handles the failure without stalling the queue or leaking subsequent submitted tasks.
343+
*/
344+
public void testRecoversFromReentrantRejectedExecution() throws Exception {
345+
Executor rejectingExecutor =
346+
task -> {
347+
throw new RejectedExecutionException();
348+
};
349+
350+
TestCallable secondCallable = new TestCallable(immediateVoidFuture());
351+
TestCallable thirdCallable = new TestCallable(immediateVoidFuture());
352+
AtomicReference<ListenableFuture<@Nullable Void>> failingFuture = new AtomicReference<>();
353+
AtomicReference<ListenableFuture<@Nullable Void>> thirdFuture = new AtomicReference<>();
354+
355+
// Submit an outer task using directExecutor.
356+
// Inside its execution, we submit Task 2 (rejectingExecutor) and Task 3 (directExecutor)
357+
// reentrantly to test reentrant queue draining error handling.
358+
Future<?> unused =
359+
serializer.submitAsync(
360+
() -> {
361+
// Reentrant submission of Task 2 to rejectingExecutor.
362+
failingFuture.set(serializer.submitAsync(secondCallable, rejectingExecutor));
363+
// Reentrant submission of Task 3 to directExecutor.
364+
thirdFuture.set(serializer.submitAsync(thirdCallable, directExecutor()));
365+
return immediateVoidFuture();
366+
},
367+
directExecutor());
368+
369+
// Verify reentrant queue recovery: Task 3 runs successfully despite Task 2's executor
370+
// rejection.
371+
assertThat(secondCallable.called).isFalse();
372+
assertThrows(ExecutionException.class, () -> getDone(failingFuture.get()));
373+
assertThat(thirdCallable.called).isTrue();
374+
assertThat(thirdFuture.get().isDone()).isTrue();
375+
}
376+
299377
@SuppressWarnings("ObjectToString") // Intended behavior
300378
public void testToString() {
301379
Future<?> unused = serializer.submitAsync(firstCallable, directExecutor());

0 commit comments

Comments
 (0)