CB-528: close the recovery race in AmqpReplyInbox #88
Reference in New Issue
Block a user
Delete Branch "worker/cb528-recovery-race"
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?
Follow-up to PR #83 (CB-527/CB-528). An independent reviewer found the recovery sweep racing a concurrent publish.
Finding 1 (the race):
failPendingPublishesOnRecoverywalked and clearedpendingBySeq/pendingByMsgIdwithout holdingpublishChannelLock. Apublish()that registered while the sweep was still iterating could be failed even though it went out successfully on the already-recovered channel — a successful publish reported as failed. SinceMessageService.replymints a freshmsgIdper retry, dedup-by-msgIdcannot catch the resulting duplicate reply.Fix: guard
failPendingPublishesOnRecoverywithpublishChannelLock, the same lockpublish()holds for its seq/map-put/basicPublish(it awaits the confirm outside the lock, so no deadlock — the sweep can only ever wait for an in-flightbasicPublishcall to return, never a broker round trip).Finding 2:
close()now fails in-flight publishes immediately with a clear message instead of leaving them to idle out the 10sCONFIRM_TIMEOUT_MS.Finding 3: documented (not guarded) —
pendingByMsgIdassumesmsgIduniqueness per in-flight publish; not reachable today since the only caller generates a fresh UUID per call.Test:
AmqpReplyInboxRecoveryRaceTestdrivesfailPendingPublishesOnRecoveryand a realpublish()against each other directly (Proxy-backed fake AMQP channels, no live broker reconnect needed) with a large in-flight backlog to make the race window observable. Confirmed it fails without the fix (reply "fresh" wrongly failed as "connection recovered mid-publish") and passes reliably with it. Also coversclose()'s fail-fast behavior.mvn -f bridged/pom.xml clean install: Tests run: 809, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.mvn test -Pcontract -Dtest=AmqpReplyInboxContractTest: Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS.Correction to my merge message. I wrote there that "#83's own new tests are contract-tagged, so the default suite and CI prove nothing about them." The first half is right; the claim about CI is wrong.
.gitea/workflows/ci.ymlhas a dedicatedcontractjob (CB-521, lines 64–97) that runsmvn -B -Pcontract test -Dtest=AmqpReplyInboxContractTestagainst a realrabbitmq:3.13service container, withAMQP_URIpointed at it. So CI does cover these tests — the split is deliberate and documented in the workflow's own comments: the default job excludes thecontractgroup because it has no broker, and the second job supplies one.The merge stands, and my local contract run was still a real check rather than a redundant one — it confirmed the fix before the merge rather than after. But the reasoning I gave for it was wrong, and anyone reading that message would conclude this repo has a CI gap it does not have.