fleetd #726 unit 3: make LeadRollover.confirm() single-flight per lead terminal #733
Closed
agent
wants to merge 0 commits from
worker/726-ea34a0-2 into main
pull from: worker/726-ea34a0-2
merge into: fleet:main
fleet:main
fleet:worker/726-unit2-75cb13-4
fleet:worker/737-owner-key-ff061f-10
fleet:worker/736-presence-forget-f35144-9
fleet:worker/705-observer-14c258-6
fleet:worker/722-024c34-5
fleet:worker/726-10cbf0-1
fleet:worker/729-5961c6-3
fleet:worker/727-ee14ed-3
fleet:worker/719-bdd95e-4
fleet:worker/702-4f5c7f-2
fleet:worker/715-5c43fc-1
fleet:worker/721-70f9ea-5
fleet:worker/718-99362b-2
fleet:worker/task-15-af0d10-12
fleet:worker/task-16-50a702-13
fleet:worker/task-12-4d0479-9
fleet:worker/task-13-823ce2-10
fleet:worker/705-ticket-owner-af9928-8
fleet:worker/703-list-collaborators-9c06c2-7
fleet:worker/669-example-truth-0b303d-6
fleet:worker/669-collab-deliverability-9ba859-3
fleet:worker/669-collab-reload-report-2a21bd-4
fleet:worker/669-7e80a6-1
fleet:worker/669-unit-d-efbbd7-1
fleet:worker/669-1b786a-1
fleet:worker/669-1d1d9f-1
fleet:worker/692-4afb9d-2
fleet:worker/689-02fced-13
fleet:worker/693-cf23fa-14
fleet:worker/677-fix-lead-collision-f69073-12
fleet:worker/638-fix-overmask-dbb1bf-11
fleet:worker/675-5b7478-4
fleet:worker/669-unit-a-70cc8f-3
fleet:worker/677-8cdaaf-5
fleet:worker/638-a7b391-1
fleet:worker/683-4536d6-2
fleet:worker/651-a75bbe-8
fleet:worker/680-20607d-7
fleet:worker/664-c12e95-3
fleet:worker/668-08534d-4
fleet:worker/672-0f2469-2
fleet:worker/670-7d1022-1
fleet:worker/661-ac7c28-2
fleet:worker/664-37fb9b-3
fleet:worker/663-remove-3arg-read-3f6783-1
fleet:worker/659-remove-dead-backcompat-ba5e6f-1
fleet:worker/637-revision-60a488-23
fleet:worker/656-redact-regression-tests-892903-19
fleet:worker/637-context-gauge-threshold-466eb5-16
fleet:worker/639-redact-line-numbers-de4ac4-17
fleet:worker/641-set-reformat-guard-6f96a4-18
fleet:worker/642-herdr-guard-scope-5de0e4-15
fleet:worker/650-javadoc-scope-95f3b3-14
fleet:worker/612-01e9f7-13
fleet:worker/612-a-r4-quarantine-outage-7ab0e8-5
fleet:worker/612-a-r9-r11-capacity-coverage-peers-cfcc79-7
fleet:worker/612-a-r10-loophealth-ccc872-8
fleet:worker/612-a-r12-turnregistrar-9e3bb7-9
fleet:worker/612-a-r5-leadconfigdir-9e70cf-6
fleet:lead/config-edit-redact-anchor-wording
fleet:worker/config-edit-seam-ca8dc1-1
fleet:worker/612-r67-630-lifecycle-290b8d-3
fleet:worker/629-625-ports-seams-da7d5d-4
fleet:worker/612-r12-exhaustion-f37cd7-1
fleet:worker/612-r38-amqp-24b083-2
fleet:worker/fleetd-612-unita-87807e-1
fleet:worker/612-b3-mcpwirings-da2b58-3
fleet:worker/612-b2-cb185-176d3a-2
fleet:worker/612-b1-completion-457459-1
fleet:worker/612-agaps-73a926-2
fleet:worker/608-sleeps-3a64ff-3
fleet:worker/621-b4520b-1
fleet:worker/618-b83894-2
fleet:worker/fleetd-615-e05481-5
fleet:worker/lead-autocompact-5f1ab2-3
fleet:worker/fleetd-613-f85deb-3
fleet:worker/fleetd-608-flaky-nudge-test-d0c2d1-3
fleet:worker/lead-context-gauge-ad404f-1
fleet:worker/gauge-wiring-9158c1-4
fleet:worker/redeploy-slowstart-ead0e5-5
fleet:worker/charter-bytes-13668c-6
fleet:worker/rollover-outcome-291483-2
fleet:worker/589-f64303-2
fleet:worker/593-1a8025-5
fleet:worker/589-fcd2aa-1
fleet:worker/568-9fdaa2-3
fleet:worker/571-attempted-outcome-5739f7-2
fleet:worker/581-completionresolver-cas-sites-0542b7-6
fleet:worker/562-loop-health-wiring-test-99611c-5
fleet:worker/562-surface-loop-health-7df5cc-4
fleet:worker/575-waiter-cleanup-sites-62ad80-1
fleet:worker/572-answer-lock-release-46a9ae-5
fleet:worker/567-probe-channel-leak-a38fc5-6
fleet:worker/551-record-before-send-7cbf56-1
fleet:worker/561-listener-fanout-survives-a-throw-61d538-2
fleet:worker/555-redeploy-main-flow-seam-65c2f5-2
fleet:worker/556-injector-owns-registration-e027a5-1
fleet:worker/552-post-restart-mktemp-abort-bc2672-4
fleet:worker/553-onstatus-completion-leak-0da881-2
fleet:worker/550-shasum-linux-196132-1
fleet:worker/538-loop-dies-on-error-4a5eeb-6
fleet:worker/426-health-coverage-ef1fd4-4
fleet:worker/504-failed-reported-clean-3cfd66-3
fleet:worker/537-capturedlog-close-e4c437-2
fleet:worker/459-broken-link-targets-cadc17-5
fleet:worker/535-appender-leak-fe74c1-1
fleet:worker/512-part2-shutdown-detection-434701-9
fleet:worker/529-logger-level-sweep-2a5533-8
fleet:worker/528-drain-gate-call-site-5de83d-7
fleet:charter/forge-mcp-vs-token
fleet:worker/521-swap-guard-unpinned-28e931-5
fleet:worker/519-probe-test-harness-d25ab8-4
fleet:worker/525-logger-level-leak-1b4eb0-6
fleet:worker/518-fleetmcp-resolver-wiring-8ef96c-1
fleet:worker/512-drain-complete-line-7edd71-3
fleet:worker/517-abort-branch-and-jar-id-41b641-2
fleet:worker/500-9e52c9-3
fleet:worker/509-4912f4-2
fleet:worker/511-9a4b23-1
fleet:worker/493-479f45-2
fleet:worker/505-03f8b2-1
fleet:worker/492-followup-detect-unclear
fleet:worker/501-a31fa0-7
fleet:worker/498-451d1c-5
fleet:worker/494-1015ce-2
fleet:worker/492-209647-1
fleet:worker/489-001902-2
fleet:worker/480-relative-handover-path-906323-1
fleet:worker/480-b-handover-skill-45bf1f-5
fleet:worker/474-followup-source-pin-f54a55-17
fleet:worker/474-charter-check-on-reload-f54a55-17
fleet:worker/466-quarantine-repeatcount-report
fleet:worker/393-opencode-skill-seeding-71854b-13
fleet:worker/469-canonical-tool-names-2a472a-16
fleet:worker/466-quarantine-escalation-5ae9c1-15
fleet:worker/446-hot-exhausted-pattern-0af580-6
fleet:worker/464-charter-tool-name-guard-a85635-12
fleet:worker/463-listfleet-default-fails-open-f1c76c-11
fleet:worker/458-invariant-5-by-purpose-862f9a-10
fleet:worker/439-coordinator-row-gate-bc032a-8
fleet:worker/449-herdr-protocol-576015-4
fleet:worker/450-abstract-spawn-599e1c-5
fleet:worker/437-ack-refuses-177d91-1
fleet:worker/444-placement-window-feb56a-2
fleet:worker/440-helddurable-derived-d462d7-13
fleet:worker/425-rework-placement-resolve-c58ba1-9
fleet:worker/421-lead-peek-held-msgs-cdbad2-10
fleet:worker/435-fixed-policy-cap-fe11de-12
fleet:worker/422-gate-state-observability-9e79d6-11
fleet:worker/431-memberregistry-live-readers-cdbad2-10
fleet:worker/424-architect-slot-hot-038b41-7
fleet:worker/422-model-gate-spawn-c29f48-6
fleet:worker/425-default-profile-live-f55534-8
fleet:worker/415-coverage-wording-2cbf9c-5
fleet:worker/416-3ad1da-1
fleet:worker/418-588283-3
fleet:worker/deterministic-stamp-race-409-3cb7b6-10
fleet:worker/armed-reads-live-config-404-ed931f-9
fleet:worker/reply-peer-refusal-391-5a34bd-7
fleet:worker/models-allowlist-aa9e9b-3
fleet:worker/ttl-stamp-race-399-f1122f-8
fleet:worker/scrub-receipt-400-316b3e-5
fleet:worker/exhaustion-detection-395-105105-6
fleet:worker/scrub-abort-394-316b3e-5
fleet:fix/scrub-uid-abort
fleet:worker/task-scrub-517574-2
fleet:worker/t386-clock-bd5b78-4
fleet:worker/t384-scrub-813790-5
fleet:worker/t381-cc-748314-2
fleet:worker/t373-336973-2
fleet:worker/t365-3920c5-3
fleet:worker/t358-6e989b-1
fleet:worker/t355-8b321c-1
fleet:worker/fleetd-369-hermetic-git-tests-e8b19a-3
fleet:worker/fleetd-368-stale-lead-binding-f5682e-2
fleet:worker/fleetd-360-deploy-units-0d3793-1
fleet:worker/359-dead-lead-tabs-f1253b-4
fleet:worker/362-worktree-skills-c03e51-3
fleet:worker/361-coord-visibility-655144-1
fleet:362-plugin-visibility-and-drift
fleet:worker/errscan-bed2ca-2
fleet:worker/amqp-log-identity-bed2ca-2
fleet:worker/withdefaults-guard-561704
fleet:worker/sleepguard-82076d-1
fleet:worker/fd334-9ee1b6-5
fleet:worker/fd348-f1ab27-4
fleet:worker/fd335-a71c35-1
fleet:worker/fd342-174a17-2
fleet:worker/fd345-490d0f-3
fleet:worker/fleetd-337-5ec7d4-21
fleet:worker/fleetd-341-af5a6b-24
fleet:worker/fleetd-339-5ca0a2-23
fleet:worker/fleetd-338-83a4a1-22
fleet:worker/fleetd-333-281f46-18
fleet:worker/fleetd-329-11bdbb-16
fleet:worker/fleetd-330-2770fb-17
fleet:worker/fix-326-50506e-15
fleet:worker/fix-324-3e9bbf-14
fleet:worker/fix-323-b8287d-13
fleet:worker/fix-316b-bd0860-11
fleet:worker/fix-318-76ca36-9
fleet:worker/fix-317-486aec-8
fleet:worker/fix-315-ce47c5-6
fleet:worker/fix-307-275890-6
fleet:worker/fix-308-b4f664-7
fleet:worker/fix-309-ec3939-8
fleet:worker/fix-310-7a3974-9
fleet:worker/fix-302-52ad0e-9
fleet:worker/fix-298-ce1acb-8
fleet:worker/fix-297-66bd11-7
fleet:worker/fix-296-104622-6
fleet:worker/fix-293-bare-closetab-eb22b5-3
fleet:worker/fix-280-gone-ask-lapse-bca98e-2
fleet:worker/fix-290-reapidle-guard-coverage-9b0dd1-1
fleet:worker/fix-285-trust-seed-8f3565-10
fleet:worker/fix-284-backend-error-seat-85912c-11
fleet:worker/fix-282-chained-ask-e6d0bb-8
fleet:worker/fix-283-teardown-leaks-f40dfa-9
fleet:worker/fix-281-pin-handler-actions-4921ac-7
fleet:worker/audit-rendezvous-lifecycle-d072ae-2
fleet:worker/audit-health-placement-1a2476-6
fleet:worker/audit-teardown-exits-e207a5-3
fleet:worker/audit-launcher-asymmetry-27e370-4
fleet:worker/audit-rest-authz-6ca53c-5
fleet:worker/investigate-275-abandon-asking-fdef52-8
fleet:worker/fix-274-worktree-leak-b0095d-7
fleet:worker/fix-273-exhausted-pattern-9665b5-6
fleet:worker/fleetd-267-model-check-bd8068-1
fleet:worker/fleetd-131-archunit-18b834-7
fleet:worker/fleetd-266-sshagent-rename-a014ff-6
fleet:worker/fleetd-184-uid-claim-8e1f31-4
fleet:worker/fleetd-184-warn-b381ee-10
fleet:worker/fleetd-184-docs-be1d12-9
fleet:worker/fleetd-257-9bf010-7
fleet:worker/fleetd-103-23a113-6
fleet:worker/fleetd-247-342356-5
fleet:worker/fleetd-116-04dea8-4
fleet:worker/fleetd-252-a830e0-3
fleet:worker/fleetd-111-7e8673-9
fleet:worker/fleetd-155c-f8ef4b-8
fleet:worker/fleetd-176-b928ca-3
fleet:worker/fleetd-249-7a7878-2
fleet:worker/cb248-composition-root-b-9acdf7-15
fleet:worker/cb148-envrc-default-fa6c82-12
fleet:worker/cb201-unit5-wiring-6c12e6-8
fleet:worker/cb241-fallback-echo-1175e9-11
fleet:worker/cb149-trust-dialog-2392a5-9
fleet:worker/cb134-148-overlay-visible-c9b986-10
fleet:worker/cb234-session-id-keyed-04e1fc-1
fleet:worker/cb201-unit3-nudge-abdf5c-6
fleet:worker/cb201-unit2-policy-c1102c-5
fleet:worker/cb201-unit4-outcome-a13bfa-7
fleet:worker/cb201-unit1-classifier-91b9b1-4
fleet:worker/cb201-227-refine-831980-3
fleet:worker/cb175-model-readback-0f085f-1
fleet:worker/cb222-charter-tmpdir-17f013-1
fleet:worker/cb226-architect-slot-race-cd3aa8-3
fleet:worker/cb224-worktree-root-group-024523-2
fleet:worker/cb-123-role-demotion-c600f7-2
fleet:worker/cb-219-opencode-roots-1f677e-1
fleet:worker/cb214-claude-session-id-b9eab4-4
fleet:worker/cb213-zdotdir-wrong-process-dd6de4-3
fleet:worker/cb211-exhaustion-classification-9546e0-2
fleet:worker/cb137-ambiguous-task-4df3d8-4
fleet:worker/cb209-agentsessionid-4dfdb6-2
fleet:worker/cb185-hostenvnames-2692b5-3
fleet:worker/cb206-opencode-sqlite-128718-2
fleet:worker/cb185-worktree-group-fc0c99-1
fleet:worker/cb-137-ask-ticket-e7760c-2
fleet:worker/cb-172-broker-uri-d36ae4-4
fleet:worker/cb-175-model-readback-76ead6-3
fleet:worker/cb-161-pane-ancestry-293510-1
fleet:worker/cb-164-rebase-885863-8
fleet:worker/cb-164-empty-scrape-false-success-1a80af-3
fleet:fix/cb-197-ticket-ttl-from-completion
fleet:worker/cb-189-remote-url-coverage-4692f3-1
fleet:worker/cb-185-blockers-027756-4
fleet:worker/cb-192-gap-log-11b631-2
fleet:worker/cb-633-fix-5f4396-3
fleet:worker/cb185-router-d6436d-3
fleet:worker/cb185-router-routing-gaps-9e9d33-3
fleet:worker/cb185-paneids-992586-2
fleet:worker/cb-633-allow-list-union-ed374b-1
fleet:worker/cb-157-credential-in-remote-url-496e44-2
fleet:worker/cb-641-health-herdr-evidence-8f1f54-6
fleet:worker/cb-640-health-msg-evidence-99c9cd-1
fleet:worker/cb-642-fleets-status-skill-bbbc40-5
fleet:cb-634-ide-mcp
fleet:worker/lead-comms-wiring-c014b9-7
fleet:worker/lead-mailbox-c19577-6
fleet:worker/autocompact-window-82bc2f-5
fleet:worker/cb-634-probe-18056f-4
fleet:worker/cb635-broker-urienv
fleet:worker/cb-632-config-retry-8e0efa-7
fleet:lead/cb-622e-claude-md
fleet:lead/cb-622-followup
fleet:worker/cb-622a-165dff-1
fleet:lead/cb-622d-opencode-mount
fleet:worker/cb-622b-717c67-2
fleet:worker/cb-622c-ab7759-3
fleet:worker/cb-617b2-20ca4b-3
fleet:worker/cb-617a-5c2f4a-1
fleet:worker/cb596-4e49ef-3
fleet:worker/cb586-10500c-1
fleet:worker/cb-606-b9343a-25
fleet:worker/cb604-1445f8-24
fleet:worker/cb582-477374-21
fleet:worker/cb584-8c2281-22
fleet:worker/cb600-e6b9a9-20
fleet:worker/cb602-ce257f-19
fleet:worker/cb601-b42837-18
fleet:worker/cb598-6c7ba7-17
fleet:worker/cb599-740fe4-16
fleet:worker/cb597-282224-15
fleet:worker/cb590fix-185e9a-10
fleet:worker/cb528-recovery-race
fleet:worker/cb594-96bead-8
fleet:worker/cb590-916766-2
fleet:worker/cb527-997d99-3
fleet:worker/cb592-env-leak-3cbf9c-1
fleet:worker/cb588-async-ticket-nudge-3218f7-5
fleet:worker/cb578b-9dcb13-6
fleet:worker/cb581-d24826-5
fleet:worker/m2-u5-ef8c42-15
fleet:worker/cb578a-516499-2
fleet:worker/cb576-01a04b-17
fleet:worker/cb579-lead-tab-acba06-20
fleet:worker/cb580-terminal-health-ed6058-21
fleet:worker/cb577-f36fdc-18
fleet:worker/cb573b-3db06f-16
fleet:worker/cb568c-f36fdc-18
fleet:worker/cb568-drop-cause-c3ac1c
fleet:worker/cb575-cancelled-notification-c3ac1c
fleet:worker/m4-sol-a2cbec-3
fleet:worker/cb574-async-ask-c3ac1c
fleet:worker/cb573-health-model-8ca857-14
fleet:worker/cb572-unknown-target-7f2e35-13
fleet:worker/u4-700706-9
fleet:worker/u3-b9fcb6-6
fleet:worker/u2-ef5b68-4
fleet:worker/u1-469dce-1-clean
fleet:worker/u1-469dce-1
fleet:worker/cb-564-health-events-70cf7e-2
fleet:worker/cb-565-recycle-drops-role-98e58f-3
fleet:worker/cb-563-missing-reply-df2866-1
fleet:worker/cb-562-readiness-gate-silent-6c23c9-3
fleet:worker/cb-560-architect-presence-da8155-1
fleet:worker/cb-561-architect-silent-off-a71cab-2
fleet:worker/cb-548-bind-architect-slot-fe1b8c-1
fleet:worker/parity-overlay-settings-5fb711-1
fleet:secrets-central-store
fleet:cb-559-hot-key-correction
fleet:cb-557-fleet-role-pools
fleet:worker/cb-553-maxload-explicit-spawn-305ee3-6
fleet:worker/cb-551-idle-lead-heartbeat-f1633c-1
fleet:worker/cb-544-drain-preserves-worktree-925fad-3
fleet:worker/cb-552-docs-sync-1cb9cf-4
fleet:worker/cb-548-rendezvous-guard-rebased
fleet:worker/cb-548-rendezvous-guard-116b53-10
fleet:worker/cb-548-authz-v2-586df6-8
fleet:worker/cb-548-authz-264363-5
fleet:salvage/cb-528b-codex-home
fleet:salvage/cb-528a-codex-launcher
fleet:CB-518-primary-flow
fleet:feature/peer-launcher-spi
fleet:cb-103-injector
No Reviewers
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#733
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "worker/726-ea34a0-2"
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?
What
Two
open()calls for the same lead terminal mint two tokens, both of which passconfirm()'s ownership check (p.leadTerminal().equals(callerTerminal)), so both could reachcontinuationRunner.accept(...)and roll the same lead twice. Today that costs two/clears; after fleetd #726 unit 2 turns the roll into "end the process and launch a fresh one", it becomes a race to kill and rebuild the same lead twice.Change
rollingByTerminal: Map<String, String>(lead terminal -> the token currently claiming it).confirm()claims a slot with an atomicputIfAbsent(p.leadTerminal(), token)after every other gate (NOT_CONFIGURED,UNKNOWN_TOKEN,NOT_YOUR_ROLLOVER,OPERATOR_NOT_CONFIRMED,checkHandover) has passed, and beforeoutcomes.put(...)/pending.remove(...)/continuationRunner.accept(...). A non-null previous value refuses with the newRefusalReason.ROLL_ALREADY_RUNNING, naming the lead terminal and the token holding the claim.runRolloverreleases the claim in afinally(rollingByTerminal.remove(p.leadTerminal(), p.token())), covering both the normal return and the fleetd #615 thrown-exception path, so one failed roll does not leave the lead permanently unrollable.cancel(token)is unchanged (removes frompendingonly) — covered by a new test proving it cannot release a claim already taken by a confirmed roll.Tests (LeadRolloverTest, 5 new, all existing kept)
secondConfirmForSameTerminalIsRefusedWhileFirstRollIsStillInFlight— two confirms on one terminal: first approved, secondROLL_ALREADY_RUNNING, continuationRunner ran exactly once; after the held roll finishes, a fresh confirm on the same terminal is approved.differentLeadTerminalsConfirmIndependently— two different terminals both succeed.refusedConfirmDoesNotTakeTheClaim— anOPERATOR_NOT_CONFIRMEDrefusal never claims the terminal.throwingContinuationStillReleasesTheClaim— aRuntimeExceptionout of the continuation still releases the claim (fleetd #615 interaction).cancelAfterConfirmDoesNotReleaseTheClaim—cancel()on an already-confirmed token returns false and does not free the claim.One existing test,
evictionCountsInProgressEntriesTowardTheCap, used aHoldingRunnerto putOUTCOME_HISTORY_CAP+50simultaneous in-flight rolls on the SAME lead terminal — incompatible with single-flight by construction. Changed it to use a distinct terminal per iteration so it still exercises theoutcomesbounded-eviction behaviour it was written for, without tripping the new per-terminal claim.Build
mvn -q clean installgreen.LeadRolloverTest: 50/50 passing. Project-wide (summed fromtarget/surefire-reports): 2059 run, 0 failures, 0 errors.Mutation evidence (apply / RED / revert / GREEN, each verified individually)
putIfAbsent->put:secondConfirmForSameTerminalIsRefusedWhileFirstRollIsStillInFlightfails on the final re-claim assertion (the overwritten map entry is never released by the first roll'sfinally, since it checks the two-argremove(key, value)).finallyrelease moved to the success-path only:throwingContinuationStillReleasesTheClaimfails — a fresh confirm after the throw is refusedROLL_ALREADY_RUNNINGinstead of approved.OPERATOR_NOT_CONFIRMEDcheck:refusedConfirmDoesNotTakeTheClaimfails — the retry confirm is refused instead of approved.tokeninstead ofp.leadTerminal():secondConfirmForSameTerminalIsRefusedWhileFirstRollIsStillInFlightfails — the second confirm (different token) is wrongly approved.All four reverted cleanly back to the committed source (diffed identical) with the full
LeadRolloverTestclass green (50/50) afterward.Scope note
Only
LeadRollover.javaandLeadRolloverTest.javatouched, per the ticket's unit-3 scope. Did not touchLeadLauncher.java,FleetdAssembly.java,Fleetd.java, orrunRolloverUnguarded's body (reserved for unit 2).Ref: fleetd #726 (comment #726 (comment) names this exact unit: "
LeadRollover.confirm(): single-flight claim keyed onp.leadTerminal(), released inrunRollover'sfinally").Review — verified myself, one fix wanted
My own reading of the pushed tree. Throwaway worktree on
origin/worker/726-ea34a0-2, tree hashconfirmed equal to the branch's, then
mvn clean installunpiped:Matches the report. The claim is placed correctly — after every gate including
checkHandover, beforeoutcomes.put/pending.remove/continuationRunner.accept— and released inrunRollover'sfinally, so it covers the fleetd #615 catch as well as the normal return.Two things done better than the brief asked, worth naming so they are not lost:
remove(key, value). A plainremove(key)would let a rollclear a claim it does not hold. Nothing in the brief said to do that.
evictionCountsInProgressEntriesTowardTheCappiled upOUTCOME_HISTORY_CAP + 50simultaneous rollson one terminal, which single-flight makes impossible by construction. I read the change: it swaps
LEADfor"term_cap_" + iand nothing else, so the eviction assertions and the test's purpose areintact, and the inline comment states the constraint rather than the history. That is the right
resolution and the right way to surface it. The acceptance criterion was mine and it was wrong to
say "unchanged" — a behaviour change is allowed to invalidate a test that encoded the old behaviour.
Your mutation-1 reading is also sharper than my brief expected, and correct:
putreturns the previousvalue too, so the refusal still works; what breaks is that
putoverwrites the entry with the secondtoken, so the first roll's
remove(terminal, firstToken)no longer matches and the claim leaks. Goodthat the suite catches the consequence rather than only the obvious branch.
The one fix: a throw from
continuationRunner.acceptleaks the claim permanentlyIf
acceptthrows,runRollovernever runs, so thefinallythat releases never runs either. Theclaim is held with no owner, and there is no way to clear it —
cancel()only touchespending,the token is already out of
pending, and nothing else writesrollingByTerminal. That lead terminalcan never be rolled again until the daemon restarts.
Reachability, stated honestly. With today's runner —
r -> Thread.ofVirtual().name(...).start(r)—I do not think a
RuntimeExceptionis reachable: a virtual thread start does not allocate an OSthread, and the failure it could throw is an
Error, not aRuntimeException. So I am not claiming alive defect. What makes it worth four lines now is the combination: the hole is invisible, the
consequence is unrecoverable, and it opens the moment the runner changes — a bounded executor would
throw
RejectedExecutionExceptionand every rejected roll would permanently brick its lead.This is in scope for this unit because unit 3 is what makes it unrecoverable. Before the claim, the
same throw left a stuck
IN_PROGRESS— bad, but visible instatus()and harmless to the next roll.Wrap the hand-off so a failure to start the continuation is treated like a failure inside it:
release the claim, and write a terminal
FAILEDoutcome whose detail says the continuation neverstarted, so
status()does not reportIN_PROGRESSforever. Note the asymmetry with fleetd #615 inthe commit message, not in a comment.
Add one test: a
continuationRunnerthat throws, asserting the outcome is terminal (notIN_PROGRESS) and that a freshopen()+confirm()on the same terminal is then approved. Mutation:remove the release from the new catch and show that test die.
Then re-run
mvn clean installand report the count. Nothing else in this PR needs to change.Merged locally as
a332dfd, pushed tomain. Closing by hand — we merge locally, so Gitea does not close it itself.The finding is fixed where it had to be fixed, and the fix is right in the detail that mattered.
remove(key, value), so it clears only this roll's own claim. A one-argument remove here would have been a new defect:confirm()can throw on a claim it does not hold once the map is shared.confirm()still returnsapproved(), and the failure surfaces throughstatus(). That matches how #615 already handles a throw inside the continuation, so the two rejection paths now report the same way rather than one refusing and one not.outcomes.putoverwrites theIN_PROGRESSentry written a few lines above. Without itstatus()would sayIN_PROGRESSforever for a roll that never began — the worse half of the original finding, and you fixed both halves.On reachability, stated plainly. This path is not reachable with today's virtual-thread runner, which does not reject. I asked for it anyway because the leak is unrecoverable once it happens, and it opens the moment the runner changes. The new test uses a lambda that always throws, standing in for
RejectedExecutionException, which is the honest way to cover a path no production wiring reaches yet.My own verification, not yours. I merged
origin/maininto your branch in a throwaway worktree and built the merge result.mainhad moved twice under you — #729, then unit 1 (#731) — so your branch's own build was not the merge. Merge tree6639327, byte-identical to the tree I pushed.2070 rather than your 2060, and the arithmetic agrees: 2054 base + 3 (#729) + 7 (unit 1) + 6 (this unit) = 2070.
One check I could not run.
ide_diagnosticsis unavailable right now — thefleetdproject is not open in IntelliJ, which answersproject_not_found. I am reporting the Maven build only; I have not run the IDE inspections onLeadRollover.java.On my own wrong acceptance criterion, for the record. I wrote "every existing test still passes unchanged" in the brief, and that was unsatisfiable:
evictionCountsInProgressEntriesTowardTheCappiles upOUTCOME_HISTORY_CAP + 50simultaneous rolls on one terminal, which single-flight makes impossible by construction. You flagged the tension instead of hiding it and resolved it correctly with a distinct terminal per iteration. The criterion was wrong, not your change.Unit 2 now goes on this base.
Pull request closed