Skip to content

Commit e30a59c

Browse files
committed
Fix lost query-worker dispatch under single-flight reuse
QueryWorker.runLoop() consumed the dispatch hand-off (q = current) under signalLock but cleared the slot (current = null) only after runOn() returned, outside the lock. A Query lease is single-flight but reused: the user thread loops submit() -> await() on the same handle. The terminal callback inside runOn() wakes the user thread, which can call submit() -> dispatch() -- setting current = q and signalling -- before the worker thread reaches its post-run finally block. That stale current = null then clobbered the freshly dispatched job and discarded its already-consumed signal, so the worker parked forever on the condition while the user thread blocked on a Completion that never fired. The borrowed worker never returned to the pool and the caller hung indefinitely. Clear current under signalLock at the moment of consumption and drop the post-run finally clear. dispatch() now cannot be clobbered: by the time the next dispatch runs, the worker is either already awaiting (so the signal wakes it) or will observe current != null on the while check and skip awaiting. The exception path leaves current already null, and the shutdown branch still clears under the lock. Surfaced as a 60s hang in QuestDBFacadeE2ETest.testSustainedMixedConcurrency (more threads than pool slots, repeated submit/await per lease). Was intermittent and timing-sensitive, so it showed up mainly on aarch64 CI; reproduced locally on x86 about one run in four, and 15/15 clean with this fix.
1 parent f39e846 commit e30a59c

1 file changed

Lines changed: 11 additions & 2 deletions

File tree

core/src/main/java/io/questdb/client/impl/QueryWorker.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,24 @@ private void runLoop() {
235235
return;
236236
}
237237
q = current;
238+
// Clear the hand-off slot under signalLock, at the moment of
239+
// consumption -- NOT after runOn() returns. A lease is
240+
// single-flight but reused: the user thread loops submit() ->
241+
// await() on the same handle. The terminal callback inside
242+
// runOn() wakes the user thread, which can call submit() ->
243+
// dispatch() (current = q; signal) before this worker thread
244+
// returns from runOn(). Clearing current after runOn() would
245+
// race that dispatch, clobber the freshly-set job, drop its
246+
// already-consumed signal, and park the worker forever while
247+
// the user thread waits on a Completion that never fires.
248+
current = null;
238249
} finally {
239250
signalLock.unlock();
240251
}
241252
try {
242253
q.runOn(client);
243254
} catch (Throwable t) {
244255
q.signalUnexpected(t);
245-
} finally {
246-
current = null;
247256
}
248257
}
249258
}

0 commit comments

Comments
 (0)