#318: release() no longer strands a delivery that lands while it is running #321
Closed
agent
wants to merge 0 commits from
worker/fix-318-76ca36-9 into main
pull from: worker/fix-318-76ca36-9
merge into: fleet:main
fleet:main
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-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#321
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/fix-318-76ca36-9"
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?
Fixes #318.
The bug (confirmed by reading code, not observed live)
AmqpReplyInbox.release(target)didheld.remove(target)underchannelLock, then iterated the map it took and nacked every entry.deliverCallbackinserts intoheldviacomputeIfAbsentwith nochannelLock.basicCancelstops new dispatches but does not flush a delivery already handed to the RabbitMQ client's consumer work-pool thread — so that thread can still calldeliverCallbackat any point during, or after,release()'s body. With the old code,computeIfAbsentwould find the key gone (removed) and create a brand-new map under it — onerelease()has already returned past itsremoveand will never look at again. That entry sits delivered-but-unacked on the channel until the whole inbox closes: never requeued, never redelivered, andpeek()is never called again for a released target. Commitbe5ba22(#298) is the dedicated fix for this method, but its contract test callsawaitPeekbeforerelease()runs, so it only proves the "already inheld" case, never a concurrent arrival.I checked the issue's write-up against the actual code and agree with it — I did not find anything wrong in it.
Mechanism chosen, and why
The issue offered three candidates. I picked the second one ("replace remove-then-iterate with a single atomic
held.compute(target, ...)that both takes the map and leaves behind a tombstonecomputeIfAbsentwill see") and I am not using the third ("re-checkheldonce more at the end ofrelease") — that one only narrows the window, it doesn't close it, and the issue is explicit that choosing it must be said plainly rather than called a fix. I did not choose it.Why the tombstone approach actually closes the window rather than narrowing it:
ConcurrentHashMap.compute()andcomputeIfAbsent()for the same key are mutually exclusive with each other in the JDK implementation — both go through the same per-bin locking path (including the reservation-node path used for a key's very first insert), so whichever ofrelease()'sheld.compute(target, (_, v) -> RELEASED)and a concurrentdeliverCallback'sheld.computeIfAbsent(target, ...)runs first is fully visible to the other. There is no gap for a delivery to slip through, regardless of how much wall-clock time separates the swap fromrelease()actually returning.release(): swapsheld[target]to a sharedRELEASEDsentinel viacompute()instead ofremove(), capturing whatever was previously held (via anAtomicReferencewritten inside the remapping function) to nack exactly as before.deliverCallback: aftercomputeIfAbsent, checksperTarget == RELEASED(identity, never mutates the sentinel) and nacks-with-requeue immediately instead of holding the message — mirroring the existing duplicate-delivery ack, so this does not add a wait onchannelLockacross a broker round trip (invariant 3):basicNack, like the existingbasicAckin the duplicate branch, does not await a broker reply.peek()/ack(): treat the sentinel as "nothing held" (matches "already released" semantics; avoids ever calling a mutator on the shared sentinel object).own(): clears a stale tombstone (held.remove(target, RELEASED)) when re-owning a target, so the fix can never permanently poison a target if its id is ever reused, and sohelddoesn't otherwise retain a tombstone forever for every target that has ever been released. The issue's own text says the id is never reused, so this line is defensive, not required by the issue — flagging it as a small addition beyond the strict ask, directly motivated by the tombstone this fix introduces.Deadlock/serialization check for invariant 3:
release()'s pattern is "holdchannelLock, transiently take the CHM per-key lock nested inside for thecompute()call, release it, keep going underchannelLock."deliverCallback's pattern is "take the CHM per-key lock alone forcomputeIfAbsent, release it, then separately takechannelLock." Neither thread ever holds one of these locks while blocked acquiring the other in the reverse order, so there's no lock-order cycle. It does mean a delivery for a released target can be made to wait behind an in-flightrelease()'sbasicCancel/nack round trip for that one target — same cost the duplicate-ack path already pays, and it can't affect any other target's channelLock-free normal delivery path.Invariant 2 (release() must not gain a new throw):
held.compute()doesn't throw anythingheld.remove()didn't. Invariant 4 (duplicate path): untouched, still gated by the sameperTarget.containsKey(msgId)check for the non-released case. Invariant 1 (never drop): every new nack path setsrequeue=true;peek/acknever call a mutator on the sentinel.Test — what it does and does not prove
AmqpReplyInboxReleaseRaceTest(new) forces the actual interleaving with a latch, not a sequence of calls: a fakeChannel(java.lang.reflect.Proxy, no mocking library on the classpath — matches the existingAmqpReplyInboxRecoveryRaceTestpattern) blocks the firstbasicNackcall. That call is only reachable from insiderelease()'s nack loop, which is only reached afterheld.compute(...)has already swapped in the tombstone — so observing the block is direct, ordering-guaranteed proof the tombstone is in place andrelease()has not yet returned (still holdingchannelLock). A second thread then fires the sameDeliverCallbackown()registered, for a brand-new message on the same target, whilerelease()is still stuck there.What it proves: a delivery whose
computeIfAbsentcall is ordered strictly afterrelease()'s tombstone swap, whilerelease()is still running, is nacked-with-requeue rather than silently parked forever.What it does not prove: it does not drive a real broker —
basicNackis a recorded call on the fake channel, not a verified requeue-and-redeliver. That half (a nacked-with-requeue message really does come back to a later owner) is already covered byAmqpReplyInboxContractTest.releaseCancelsConsumerAndRequeuesHeldDeliveryForRecoveryagainst a real broker, which I ran (see below) and which still passes unmodified.Mutation proof (reverted, ran, restored): I reverted
AmqpReplyInbox.javato the pre-fix version (git show HEAD:...back over the working file) and ran the new test alone. Real failure output:m0 (already held before
release()ran) gets nacked; m1 (delivered whilerelease()was still running, afterheld.removeexecuted) never does — exactly the reported defect. I then restored the fix (cpback the saved fixed file) and confirmed the full suite is green again before committing.Build
cd fleetd && mvn clean install(unpiped,$?read directly — notail/head/teein the command that produced this number):Contract suite against a real broker (Docker was available in this worktree, so I ran it rather than describing it as unrun):
mvn test -Pcontract -Dtest=AmqpReplyInboxContractTestagainst a Testcontainers RabbitMQ:All 8 pass, including
releaseCancelsConsumerAndRequeuesHeldDeliveryForRecovery(#298's own test) — the fix does not regress the case #298 already closed.Shape check (msg/, one line each, not fixed)
Requested by the issue as a separate, non-blocking check — I'll add findings as a follow-up PR comment once a background check finishes; nothing in that scope was touched by this change.
Files changed
fleetd/src/main/java/dev/ltms/fleet/msg/AmqpReplyInbox.java— the fix (release,deliverCallback,own,peek,ack)fleetd/src/test/java/dev/ltms/fleet/msg/AmqpReplyInboxReleaseRaceTest.java— new, forces the interleavingCaveat for review
The
own()tombstone-clearing line is slightly beyond the strict letter of the ticket (which only asked to fixrelease()/deliverCallback) — flagging it as a deliberate, small, closely-related addition rather than scope creep: without it, every released target'sheldmap entry becomes a permanent (if cheap) tombstone for the life of the daemon, and if a target id were ever reused (the issue says it isn't) that target would be silently nacked forever. Happy to drop it if the reviewer would rather keep this PR to exactly the two methods named in the issue.Shape check (msg/, one line each, not fixed)
Ran a read-only sweep over
src/main/java/dev/ltms/fleet/msg/for the exact shape this ticket fixed: two halves of one invariant guarded by different locks, or a lock on one side and none on the other.Same shape found — not fixed here:
MessageService.Task.turnId/.question(+asyncTasksByTurn):ask()mutates them viamarkAsyncQuestion/clearAsyncQuestion/markAskTimedOut(MessageService.java:905,908,933,934) while holding nosessionLockslock at all;answer()mutates the same task's fields viaclearAsyncQuestion/finishAsyncTask(:994,1011) while holdingsessionLocks.get(workerSession)— same key, two different synchronization regimes for what should be one consistent "this turn's question/turnId" state.MessageService.asyncTasksByTurnviafinishAsyncTask(Task,...): called fromsendAsync's background virtual thread (:1079) with no lock (the session lock was already released whensend()returned), vs.answer()'s call to the same method (:1011) withsessionLocksheld — same asymmetry as above.MessageService.strandedReplies/queuedDeliveries—send()touches them under lock,reply()/abandon()touch them unlocked. Each individual touch is a single atomic map op on a self-healing, best-effort flag (documented CB-640 semantics), not a value that can be permanently stranded, so this one reads as lower risk than the two above.Checked and found symmetric/safe:
AmqpReplyInbox.held(this PR's fix — now consistent),LeadMailbox.held,InMemoryReplyInbox.store,Rendezvous.waiters/asks/openAsksBySession(lock-free by design via atomicConcurrentHashMapops throughout),ReplyPushLoop's pending* maps (same),LeadHeartbeatLoop.idleSinceNanos/.quietCount(single-threaded by construction).Not fixing any of this — reporting per the ticket's ask.
Pull request closed