fix(qwp): fix a memory-access fault and worker leak during shutdown#65
fix(qwp): fix a memory-access fault and worker leak during shutdown#65kafka1991 wants to merge 1 commit into
Conversation
SegmentManager.close() joined the worker thread with a bounded t.join(5_000). When close() ran on a thread that already carried a pending interrupt -- the interrupted-teardown path that BackgroundDrainer deliberately leaves the status set on -- join() threw InterruptedException immediately, so the worker got no time to observe running=false and exit. close() then logged the "worker did not stop" warning and returned, leaking a live worker that still owned segment files. The leaked worker kept creating spares and trimming/unlinking sealed segments. A subsequent CursorSendEngine that mmapped one of those files raced the concurrent truncation/unlink and took a SIGBUS, surfacing as "java.lang.InternalError: a fault occurred in an unsafe memory access operation". This showed up as a flaky failure in BackgroundDrainerInterruptIsStopSignalTest. close() now clears the caller's pending interrupt before the join, waits the worker out to a deadline, and restores the interrupt on the way out so the rest of the interrupted-teardown protocol still observes it. The loop.close() latch await elsewhere stays interrupt-sensitive; only the worker join, which must reap a passive daemon that owns files, is made robust against a pending interrupt. Add testInterruptedCallerDoesNotAbandonReapableWorker, a deterministic regression that fails against the old code, and give SegmentManager a @testonly worker-join timeout seam so the existing timeout-branch test reaches its branch without the interrupt shortcut the fix removes.
[PR Coverage check]😍 pass : 14 / 16 (87.50%) file detail
|
Code review — level 3 (blocking, mission-critical pass)Scope: 2 files, +109/−7. The fix is correct. It does exactly what the commit claims: Verified against source and by executing the suite both ways: fixed code 3/3 green; with the production hunk reverted, the new regression test goes red at CriticalNone. ModerateNone. Minor1. 2. The modified test 3. 4. Downgraded (candidates dismissed after source verification)
Note (harness, cross-cutting)
SummaryVerdict: approve. A correct, narrowly-scoped fix for a real data-corruption bug (leaked worker → concurrent truncation/unlink → SIGBUS/
The only things worth changing before merge are the two test-code minors (#1 duplication, #2 which test carries the regression value), and both are optional. |
Summary
When the QWP client shuts down while a background drainer is still delivering a slot's buffered data, the drainer thread is interrupted as part of teardown.
SegmentManager.close()reaps its worker thread with a boundedt.join(...), but a pending interrupt on the calling thread makesjoin()throwInterruptedExceptionimmediately.close()then returned without the worker having stopped, leaking a live worker thread together with its native buffers.The leaked worker keeps creating hot spares and trimming/unlinking on-disk segment files. A later
CursorSendEnginethat memory-maps one of those files can race the concurrent truncation/unlink and take a SIGBUS, surfacing as:The fault is intermittent -- it only triggers when the leaked worker happens to be mutating the exact file being mapped -- and showed up as a flaky failure in
BackgroundDrainerInterruptIsStopSignalTest.Fix
SegmentManager.close()now saves and clears the caller's pending interrupt before joining the worker, waits the worker out to the same 5s deadline, then restores the interrupt so the rest of the interrupted-teardown protocol still observes it. The worker is a passive daemon that exits within microseconds ofrunning=false, so in the common caseclose()still returns immediately; the deadline is only consumed when the worker is genuinely wedged -- the same bound the original code intended before the interrupt collapsed it to zero.The change is deliberately narrow: only the worker join is hardened against a pending interrupt. The
loop.close()latch await on the same teardown path stays interrupt-sensitive by design, since it must throw under a wedged I/O thread.Tradeoffs
close()can now block up to the worker-join deadline (5s) where it previously returned at once. In practice the worker exits in microseconds; the wait only materializes when the worker is stuck, which is exactly the case where leaking it is unsafe.Tests
testInterruptedCallerDoesNotAbandonReapableWorker, a deterministic regression: it wedges the worker, callsclose()on an interrupted thread, and assertsclose()waits for and reaps the worker. It fails against the pre-fix code and passes with the fix.testCloseDoesNotFreePathScratchWhenWorkerStillAliveto reach the join-timeout branch via a new@TestOnlyworker-join-timeout seam instead of the interrupt shortcut the fix removes.SegmentManager*andBackgroundDrainer*suites pass, includingBackgroundDrainerInterruptedTeardownTest, which pins the deliberately-preserved interrupt status.