Features: the last fleet_ask timeout stranding window is closed (fleetd #334)

Dai Ha
2026-09-04 17:13:49 +07:00
parent 0f6298fc1a
commit ac507a98a1
+34
@@ -4415,3 +4415,37 @@ The first version copied the backend-error check's leading-chrome loop. Measured
left all 1369 tests green, and it must, because the scan only looks for `.`, `!` and `?` and no
chrome character is one of those. It is gone. **A step that cannot change the result is worse than
no step** — the next reader takes it as evidence that chrome was handled.
---
## The last window where a `fleet_ask` timeout stranded its ticket is closed
**What it does.** When a worker's `fleet_ask` times out, `ask()` now closes the ask turn
(`rendezvous.closeAsk`) **before** it forgets that task's `turnId` mapping. The order used to be the
other way round, with the close happening later in the shared `finally`. The whole teardown is also
gated on `ticket.fresh()` now, matching the `finally` block and the `NO_WAITER` branch, which were
already gated that way.
**On.** Always on (fleetd #334, closing what #329 only narrowed).
**Why it exists.** Between the forget and the close, the ask was still answerable while its task
mapping was already gone. A lead calling `fleet_send{turnId}` in that window got a `REPLIED` answer,
while the ticket stayed `PENDING` with no reply — forever, because nothing revisits it. Measured
with a probe before the fix: `answer=REPLIED phase=PENDING reply=null`. With the new order, an
`answer()` either sees the ask open — and then the mapping is still there — or sees it closed and
returns `STALE_TURN`. There is no state in between, because both steps run on one thread with
nothing yielding.
The `ticket.fresh()` gate fixes a second door into the same failure. A coalesced duplicate `ask`
passes its own `timeoutMillis`, which says nothing about whether the shared ask is done, so a
duplicate timing out first could lapse an ask the fresh owner was still holding.
**One thing to know for maintenance.** The two halves are pinned by two different tests, and the
second one only exists because the first did not cover it. Measured on merge: removing the
`ticket.fresh()` gate while keeping the new order left all 1371 tests green. The gate shipped with
the reorder and nothing held it there. `aCoalescedDuplicateAskTimingOutLeavesTheFreshOwnersAskOpen`
now does, and `aLateAnswerDuringAskTimeoutTeardownStillCompletesTheAsyncTicket` covers the ordering.
`MessageService` now carries six test-only hooks, one per race of this kind. Each exists because its
window is unreachable through the public API — which is also why each bug was invisible — but six is
enough that the next one needs a harder look than "the file already does this".