fleetd #553: register the rendezvous waiter in onStatus's finally backstop #557

Merged
ltms merged 2 commits from worker/553-onstatus-completion-leak-0da881-2 into main 2026-09-12 10:50:25 +02:00
Member

Fixes fleetd #553.

The uncommitted try/finally around onStatus's post-monitor region (from a previous, released worker session) completed sent.delivered() when an earlier listener threw, but never registered sent.token().waiter() -- that waiter is registered only by turnListener.onDelivered(), inside the very if (sent != null) block the finally backstops. That converted a hang into a hang WITH a success receipt: the caller was told its send landed, then waited out its full timeout for an answer that could never resolve.

The finally now does the block's whole job on the unhandled path: it calls onDelivered() (only when sendError == null) before completing the future, wrapped in its own try/catch(Throwable) so a failure there cannot mask the original throwable, which must still escape onStatus to StatusPoller's catch (Throwable). A sentHandled flag records the INTENT to handle before any side effect (set true at the start of the normal block, not after) so a throw partway through onDelivered cannot trigger a second, late captureBaseline that would permanently suppress the turn's completion (CompletionResolver's baseline-equals-tail suppression at :364-369).

Also removed a leftover duplicated forget.accept(target) call (with a stray 'MUTATION-TEST-3' comment) in the notReady block -- residue from the previous worker's own mutation testing that was not fully reverted.

Tests: two new acceptance tests added --

  1. aRuntimeExceptionFromOnTurnCompleteStillRegistersTheRendezvousWaiterForTheNextDelivery -- asserts CompletionResolver.inFlight(target) carries the delivered turn's waiter after an earlier listener throws, and that the original throwable still escapes onStatus.
  2. anOnDeliveredThrowAfterItsOwnRegistrationDoesNotRunASecondTime -- asserts onDelivered is called exactly once even when it throws after its own registration ran.

Both mutation-tested (line-anchored sed, pristine anchor 1->0, red with the test's own message, restored to byte-identical shasum -a 256, green again).

Full build: mvn clean install -- BUILD SUCCESS, Tests run: 1728, Failures: 0, Errors: 0, Skipped: 0.

Fixes fleetd #553. The uncommitted try/finally around onStatus's post-monitor region (from a previous, released worker session) completed sent.delivered() when an earlier listener threw, but never registered sent.token().waiter() -- that waiter is registered only by turnListener.onDelivered(), inside the very if (sent != null) block the finally backstops. That converted a hang into a hang WITH a success receipt: the caller was told its send landed, then waited out its full timeout for an answer that could never resolve. The finally now does the block's whole job on the unhandled path: it calls onDelivered() (only when sendError == null) before completing the future, wrapped in its own try/catch(Throwable) so a failure there cannot mask the original throwable, which must still escape onStatus to StatusPoller's catch (Throwable). A sentHandled flag records the INTENT to handle before any side effect (set true at the start of the normal block, not after) so a throw partway through onDelivered cannot trigger a second, late captureBaseline that would permanently suppress the turn's completion (CompletionResolver's baseline-equals-tail suppression at :364-369). Also removed a leftover duplicated forget.accept(target) call (with a stray 'MUTATION-TEST-3' comment) in the notReady block -- residue from the previous worker's own mutation testing that was not fully reverted. Tests: two new acceptance tests added -- 1. aRuntimeExceptionFromOnTurnCompleteStillRegistersTheRendezvousWaiterForTheNextDelivery -- asserts CompletionResolver.inFlight(target) carries the delivered turn's waiter after an earlier listener throws, and that the original throwable still escapes onStatus. 2. anOnDeliveredThrowAfterItsOwnRegistrationDoesNotRunASecondTime -- asserts onDelivered is called exactly once even when it throws after its own registration ran. Both mutation-tested (line-anchored sed, pristine anchor 1->0, red with the test's own message, restored to byte-identical shasum -a 256, green again). Full build: mvn clean install -- BUILD SUCCESS, Tests run: 1728, Failures: 0, Errors: 0, Skipped: 0.
agent added 1 commit 2026-09-12 10:36:22 +02:00
fleetd #553: register the rendezvous waiter in onStatus's finally backstop, not just the delivery future
CI / contract (pull_request) Successful in 50s
CI / build (pull_request) Successful in 2m12s
d4a51c6274
The previous try/finally around onStatus's post-monitor region completed
sent.delivered() but never registered sent.token().waiter() when an earlier
listener threw. That waiter is registered only by turnListener.onDelivered(),
inside the very if (sent != null) block the finally backstops, so a caller
was told its send landed and then waited out its full timeout for an answer
that could never resolve (worse than a plain hang).

The finally now does that block's whole job on the unhandled path: it calls
onDelivered() (when sendError == null) before completing the future, guarded
by its own try/catch(Throwable) so a failure there cannot mask the original
throwable. A sentHandled flag, set true at the START of the normal block
(before any side effect), tells the finally whether that already ran, so a
throw partway through onDelivered cannot trigger a second, late captureBaseline
that would permanently suppress the turn's completion.

Also removed a leftover duplicated forget.accept(target) call (with a stray
'MUTATION-TEST-3' comment) in the notReady block — residue from the previous
worker's own mutation testing that was not fully reverted.
agent added 1 commit 2026-09-12 10:45:48 +02:00
fleetd #553: split sentHandled's two meanings so onDelivered's own throw still completes the future
CI / contract (pull_request) Successful in 54s
CI / build (pull_request) Successful in 1m43s
8b4320ed24
Lead review of PR #557 (ticket comment 16916) found one path left open: sentHandled
is set to true BEFORE onDelivered() runs (correctly, per the earlier fix), so when
onDelivered() itself throws on the normal path, the finally's 'if (sent != null &&
!sentHandled)' guard skipped the whole recovery -- completion included -- and left
sent.delivered() pending forever for a message that really was delivered.

sentHandled must guard only the onDelivered RE-CALL (the permanent-suppression
hazard), never the future completion, since CompletableFuture.complete/
completeExceptionally are idempotent and a no-op on the already-handled path.
Split the one flag's two jobs: the outer 'if (sent != null)' now always runs the
recovery block, and '!sentHandled' moved onto just the onDelivered call inside it.

Added anOnDeliveredThrowOnTheNormalPathStillCompletesTheDeliveryFuture, proven with
the lead's own mutation (reverting !sentHandled onto the outer if): the new test
goes red while anOnDeliveredThrowAfterItsOwnRegistrationDoesNotRunASecondTime stays
green, showing the two concerns are genuinely separate.
ltms merged commit f606fccf7f into main 2026-09-12 10:50:25 +02:00
Sign in to join this conversation.