Skip to content

Commit cdc892a

Browse files
andimarekclaude
andcommitted
Fix race condition in CompletionStageSubscriber.onComplete()
The spec317_mustSupportACumulativePendingElementCountUpToLongMaxValue TCK test intermittently hangs on CI (seen on Java 21). Root cause: a race between onComplete() and finallyAfterEachPromiseFinishes() where the queue-emptiness check and the onCompleteRun read/write are not atomic — the last CF can complete between the check and the set, leaving the runnable stranded forever. Fix by extending the existing ReentrantLock scope to also cover onCompleteRun, making the emptiness check and runnable set/claim a single atomic operation on both sides. Runnables are executed outside the lock to avoid deadlocks with user code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 044d048 commit cdc892a

2 files changed

Lines changed: 21 additions & 27 deletions

File tree

src/main/java/graphql/execution/reactive/CompletionStageOrderedSubscriber.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ protected void whenNextFinished(CompletionStage<D> completionStage, D d, Throwab
3232
emptyInFlightQueueIfWeCan();
3333
}
3434
} finally {
35-
boolean empty = inFlightQIsEmpty();
36-
finallyAfterEachPromiseFinishes(empty);
35+
finallyAfterEachPromiseFinishes();
3736
}
3837
}
3938

src/main/java/graphql/execution/reactive/CompletionStageSubscriber.java

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,19 @@ protected void whenNextFinished(CompletionStage<D> completionStage, D d, Throwab
9595
downstreamSubscriber.onNext(d);
9696
}
9797
} finally {
98-
boolean empty = removeFromInFlightQAndCheckIfEmpty(completionStage);
99-
finallyAfterEachPromiseFinishes(empty);
98+
removeFromInFlightQ(completionStage);
99+
finallyAfterEachPromiseFinishes();
100100
}
101101
}
102102

103-
protected void finallyAfterEachPromiseFinishes(boolean isInFlightEmpty) {
104-
//
105-
// if the runOnCompleteOrErrorRun runnable is set, the upstream has
106-
// called onComplete() already, but the CFs have not all completed
107-
// yet, so we have to check whenever a CF completes
108-
//
109-
Runnable runOnCompleteOrErrorRun = onCompleteRun.get();
110-
if (isInFlightEmpty && runOnCompleteOrErrorRun != null) {
111-
onCompleteRun.set(null);
103+
protected void finallyAfterEachPromiseFinishes() {
104+
Runnable runOnCompleteOrErrorRun = lock.callLocked(() -> {
105+
if (inFlightDataQ.isEmpty()) {
106+
return onCompleteRun.getAndSet(null);
107+
}
108+
return null;
109+
});
110+
if (runOnCompleteOrErrorRun != null) {
112111
runOnCompleteOrErrorRun.run();
113112
}
114113
}
@@ -149,11 +148,15 @@ public void onComplete() {
149148
}
150149

151150
private void onComplete(Runnable doneCodeToRun) {
152-
if (inFlightQIsEmpty()) {
153-
// run right now
154-
doneCodeToRun.run();
155-
} else {
151+
boolean runNow = lock.callLocked(() -> {
152+
if (inFlightDataQ.isEmpty()) {
153+
return true;
154+
}
156155
onCompleteRun.set(doneCodeToRun);
156+
return false;
157+
});
158+
if (runNow) {
159+
doneCodeToRun.run();
157160
}
158161
}
159162

@@ -163,12 +166,8 @@ protected void offerToInFlightQ(CompletionStage<?> completionStage) {
163166
);
164167
}
165168

166-
private boolean removeFromInFlightQAndCheckIfEmpty(CompletionStage<?> completionStage) {
167-
// uncontested locks in java are cheap - we don't expect much contention here
168-
return lock.callLocked(() -> {
169-
inFlightDataQ.remove(completionStage);
170-
return inFlightDataQ.isEmpty();
171-
});
169+
private void removeFromInFlightQ(CompletionStage<?> completionStage) {
170+
lock.runLocked(() -> inFlightDataQ.remove(completionStage));
172171
}
173172

174173
/**
@@ -186,10 +185,6 @@ private void cancelInFlightFutures() {
186185
});
187186
}
188187

189-
protected boolean inFlightQIsEmpty() {
190-
return lock.callLocked(inFlightDataQ::isEmpty);
191-
}
192-
193188
/**
194189
* The two terminal states are onComplete or onError
195190
*

0 commit comments

Comments
 (0)