fleetd #556: make turn registration structural, independent of any TurnListener #566
Reference in New Issue
Block a user
Delete Branch "worker/556-injector-owns-registration-e027a5-1"
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?
fleetd #556: the Injector owns the invariant "every delivered turn has a registered waiter," but the only thing that satisfied it was
CompletionResolver.captureBaseline, called from inside aTurnListener.onDeliveredcallback wired inFleetd.java. AnyTurnListenerthat throws (fromonDeliveredor elsewhere) could break the invariant with no way for theInjectorto detect it. #553 only made the one reachable listener behave via a try/finally backstop; it did not remove this structural dependency.Design
Added a narrow
TurnRegistrarfunctional interface, deliberately decoupled fromTurnListener(its own javadoc explains why).CompletionResolvernow implements it via a newregister()method extracted fromcaptureBaseline's registration half;captureBaselinekeeps its own full body unchanged (only javadoc updated), so every existing direct caller/test ofcaptureBaselineis untouched.Injectorgets an explicitregistrarfield and a constructor family that auto-derives the registrar from theTurnListenerviainstanceofwhere that still works, plus new explicit-registrar overloads where it can't (Fleetd.java's anonymous fan-out listener can't implement two interfaces vianew Type(){}). The delivery path callsregistrar.register(target, sent.token())directly and unconditionally — in both the ordinaryif (sent != null)block and the #553finallybackstop — beforeturnListener.onDelivered(...), so registration no longer depends on that notification callback succeeding.Fleetd.javawirescompletion::registerexplicitly as the registrar, bypassing theturnListenerfan-out for registration purposes.CB-116 ordering (
onTurnCompletereads the PREVIOUS turn'sinFlightentry before the new turn'sregistrar.register()runs) and the two-arginFlight.remove(target, turn)vs one-arg distinction on the completion path are both preserved unchanged — see the updated comments aroundInjector.java's delivery method.#561's order test
Per comment 16966: checked
git log,git branch -a, and grepped the repo at this branch's point offmain(4f9aba4) — #561's order-dependent test (assertingFleetd.java:514-515'scompletion.onDelivered->sessions.onDeliveredcall order) does not exist anywhere in this repository. Nothing to delete. The two new acceptance tests below (a throws-from-every-callback listener, and the CB-116 order pin) are the order-independent replacement comment 16966 asked for.Tests added
InjectorTest.aTurnListenerThatThrowsFromEveryCallbackStillLeavesTheDeliveredTurnRegisteredAndResolvable— acceptance criterion 1: aTurnListenerthat throws from every callback still leaves a delivered turn registered and its waiter resolvable; red against the pre-fix shape (registration used to live insideonDelivered, one of the throwing callbacks).InjectorTest.theNewTurnsRegistrationRunsAfterThePreviousTurnsCompletionIsRead— acceptance criterion 2 (CB-116 guard): pins the call order betweenonTurnComplete(previous turn) andregister(new turn) within oneonStatuscycle.CompletionResolverTest.aSupersededTurnsPlainCompletionMustNotEvictItsSuccessorsRegistrationCompletionResolverTest.aSupersededTurnsEchoedNoReportSubPathMustNotEvictItsSuccessorsRegistrationCompletionResolverTest.aSupersededTurnsFailMustNotEvictItsSuccessorsRegistrationThese three (per comment 16908) name and test the invariant the two-arg
remove(key, value)idiom maintains — "a superseded turn's terminal handling must not evict its successor's registration" — acrossresolve()'s plain-completion branch, its echoed-noReportMessagesub-path, andfail(), rather than testing the idiom literally.Every new test was proven by mutation: a line-anchored
sededit on the exact production line each test protects (confirmed red with that test's own assertion message and observed value), restored and verified byte-identical viashasum -a 256, then re-run green as a control. Full detail is in the worker's handoff.Build
mvn -o clean install: exit 0,Tests run: 1749, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS.Scope
Only #556. #553's ordering decision was not reopened;
LoopWatchdog's package placement was not touched.Rework (comment 17009): the #553 finally backstop was unpinned
Confirmed the gap as reported:
Injector.java:703(the fleetd #553finallybackstop's ownregistrar.register(target, sent.token())call) had no test asserting registration on that path — only the delivered-future completion was pinned there (by the pre-existingaRuntimeExceptionFromOnTurnCompleteStillCompletesTheNextDelivery). Mutating:703alone (comment out, exact-line anchor 1 -> 0) left the full suite green (1749/1749 before this rework), because nothing exercised the invariant on that specific path.Fix
Added
InjectorTest.aRuntimeExceptionFromOnTurnCompleteStillLeavesTheNextDeliveryRegisteredOnTheRecoveryPath: same construction as the existing #553 test (onTurnCompletethrows for the previous turn, forcing the next turn's delivery down the finally backstop, since the ordinaryif (sent != null)path never runs for it), plus the assertion the ordinary-path test already makes —completion.inFlight(target)non-null, carrying the correct waiter — now checked on the recovery path.Mutation proof (Injector.java:703)
registrar.registersites textually distinct so this is unambiguous)InjectorTest.aRuntimeExceptionFromOnTurnCompleteStillLeavesTheNextDeliveryRegisteredOnTheRecoveryPath, exit 1:shasum -a 256onInjector.javareturns to8fcb698afccc254b0c99d3a4bf9e960c0e7c85e542024e870c1e815dce6d62a9(matches the tree before the mutation, and the sha the lead independently confirmed on52f018e).Tests run: 1, Failures: 0, Errors: 0, Skipped: 0.Full suite after the rework
mvn -o clean install: exit 0. Maven's own summary line and an independent sum overtarget/surefire-reports/*.txtagree:Tests run: 1750, Failures: 0, Errors: 0, Skipped: 0.BUILD SUCCESS.Criterion-5 caveat (restated, per item 4)
The literal form of criterion 5 — compile and run the new order-independent test against the unfixed tree at the branch point — is not executable:
aTurnListenerThatThrowsFromEveryCallbackStillLeavesTheDeliveredTurnRegisteredAndResolvablecalls the new 5-argInjectorconstructor (with the explicitTurnRegistrarparameter), which does not exist onorigin/mainbefore this ticket'sTurnRegistrar/constructor changes — the test cannot even compile against that tree. The faithful equivalent I ran instead (and reported in the original hand-off, independently reproduced by the lead on52f018e): with the fix's production code present, comment out the ordinary-path registration call (Injector.java:660, exact-line anchor 1 -> 0) so the tree reaches the same end state (no registration) that the pre-fix code produced whenever the sole listener threw. Both new tests went red against that mutation with their own assertion messages. This is stated here explicitly rather than implying the literal unfixed-tree check was run.Commit:
738d34aonworker/556-injector-owns-registration-e027a5-1.