fleetd #418: barrier the throw-path push-loop test on state decide() reads #419
Reference in New Issue
Block a user
Delete Branch "worker/418-588283-3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
MessageServiceTest.anAskThatLeavesByThrowingStillClosesItsQuestionfails under heavy host load:expected: <INJECT> but was: <STOP>. That assertion runs before the interrupt, so it has nothing to do with the throwing path the test is named for.MessageService.ask()does three things in order:markAsyncQuestion(...)— flipspoll()toPhase.ASKINGrendezvous.resolveQuestion(...)pushLoop.onQuestionOpened(...)— the step that actually populatesReplyPushLoop.pendingQuestions, whichdecide()readsThe test's old barrier,
awaitTicketPhaseOn(service, ticket, Phase.ASKING), only waits for step 1. Under load the asker thread can be descheduled between step 1 and step 3, so the barrier releases early anddecide()correctly sees nothing pending yet ->STOP.Fix (test-only, no production behaviour changed)
ReplyPushLoop#pendingQuestionTurnIdsForTest(String lead)— a package-private test seam exposing the existing privatependingQuestionTurnIdsFor, modeled onMessageService#isCompletionStampedForTest(#399). Carries no production behaviour; nothing inReplyPushLoopcalls it.turnIdfrom thePhase.ASKINGTaskView(set in step 1, so it's already valid), then waits (awaitQuestionPendingOn, 3000ms deadline, same style asawaitTicketPhaseOn/awaitCompletionStamped) for thatturnIdto actually appear in the push loop's pending-question set before assertingdecide(...) == INJECT. This barrier waits on the statedecide()actually reads (step 3), not on the asserted condition itself (decide() == INJECT) — so it stays a real assertion, not a tautology.Proof the new barrier is load-bearing (criterion 3)
Temporarily inserted
Thread.sleep(300)immediately beforepushLoop.onQuestionOpened(...)inMessageService.ask()(fleetd/src/main/java/dev/ltms/fleet/msg/MessageService.java:1005-1006), to simulate the descheduling window under load.With the OLD barrier (
awaitTicketPhaseOn(..., Phase.ASKING)only) + the injected delay -> RED:This is exactly the reported failure signature.
With the NEW barrier (
awaitQuestionPendingOn) + the same injected delay -> GREEN:(0.55s elapsed is consistent with the 300ms sleep plus the barrier's own poll.)
Restore confirmed: the temporary
Thread.sleepwas removed fromMessageService.javaafterward.git diff -- fleetd/src/main/java/dev/ltms/fleet/msg/MessageService.javais empty — production source is untouched, and the only changes in this PR are the test seam inReplyPushLoop.javaand the barrier fix inMessageServiceTest.java.Other phase-as-barrier uses checked (criterion 4)
Only the two other
awaitTicketPhaseOn(..., Phase.ASKING)call sites exist in the file (CB-582 nudge tests):anAsyncTicketThatPausesOnAQuestionNudgesTheLeadWithNoPriorPollCall(~line 1728):Phase.ASKINGis used only to capture theTaskView/turnId; the actual push-loop assertions are gated by a subsequentawaitNudge(...), which waits for a realagent.promptcall — an even stronger barrier on published push-loop state. Not exposed to this race.answeringAQuestionStopsFurtherNudgesAboutIt(~line 1757): same pattern —Phase.ASKINGonly forturnIdcapture, followed byawaitNudge(...)before any push-loop-dependent assertion. Not exposed.Other
awaitTicketPhaseOn(..., Phase.DONE)sites either don't depend on data published afterDONE(they only readTaskView.reply(), set at the same step), or already callawaitCompletionStampedafterward (the #399 fix) where the completion-stamp ordering matters. No other phase-as-barrier gap found.Build
mvn clean install(fleetd/):BUILD SUCCESS, real unpiped output:Files changed
fleetd/src/main/java/dev/ltms/fleet/msg/ReplyPushLoop.java— addedpendingQuestionTurnIdsForTesttest seam (production behaviour unchanged)fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java— newawaitQuestionPendingOnhelper;anAskThatLeavesByThrowingStillClosesItsQuestionnow barriers on push-loop state instead ofPhase.ASKING