M9-S7: fix CatalogListingView cross-queue create race#112
Merged
Conversation
Two Listings read-model handlers on different RabbitMQ queues could both LoadAsync the same catalog row as null and both Store a fresh document; the second commit silently overwrote the first, losing selling fields (Title, SellerId, StartingBid). Observed 2026-06-13. An OQ-1 spike against live Postgres refuted the planned UseNumericRevisions approach: numeric revisions bump mt_version but a plain session.Store still last-writer-wins with no exception. Enforcement needs a revision-checked write path, which sagas get from Wolverine's saga-persistence frame but plain read-model handlers calling session.Store do not have (the same lesson the AuctionClosingSaga docstrings record from M8-S3c). Fix: session.Insert on the create branch of all twelve CatalogListingView write methods across five handlers (shared IDocumentSession.InsertOrStore helper + direct Insert in the two explicit-null branches). The losing concurrent creator collides on the document primary key (DocumentAlreadyExistsException), caught by a new ListingsConcurrencyRetryPolicies retry-with-cooldown; the retried handler re-loads the committed row and merges via the existing load-and-preserve Store path, so both writers' field sets survive. No new contracts, events, or numeric-revision config. The CatalogListingView wire shape is byte-identical to baseline. 328 backend tests green (+2).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the
CatalogListingViewlast-writer-wins race between the Listings read-model handlers on different RabbitMQ queues. Two handlers could bothLoadAsyncthe same catalog row asnulland bothStorea fresh document; the second commit silently overwrote the first, losing selling fields (Title,SellerId,StartingBid). Observed live on 2026-06-13.OQ-1 spike (the gate) — reshaped the fix
The slice was gated on whether the planned
UseNumericRevisions(true)would make the losingStorethrow. A diagnostic spike against live Postgres said no:Titlemt_versionStorevsStore(current)''(lost)2InsertvsInsert(fix)DocumentAlreadyExistsException'SELLING-RECORD'(kept)1UseNumericRevisionsbumpsmt_versionbut a plainStorestill commits and last-writer-wins — enforcement needs a revision-checked write path, which sagas get from Wolverine's saga-persistence frame but plain read-model handlers do not (the same lessonAuctionClosingSagarecords from M8-S3c). So the mechanism is the document primary key, not the version column.Fix
session.Inserton the create branch of all twelveCatalogListingViewwrite methods across five handlers — via a sharedIDocumentSession.InsertOrStorehelper for the?? newsites, and directInsertin the two explicit-null branches.ListingsConcurrencyRetryPolicies : IWolverineExtensionretries onDocumentAlreadyExistsException(mirrorsAuctionsConcurrencyRetryPolicies). The retried handler re-loads the committed row and merges via the existing load-and-preserveStorepath, so both field sets survive.IRevisioned/UseNumericRevisionsedits were reverted — inert and misleading for a plain-Storepath.CatalogListingView.csis byte-identical to baseline.Scope boundary
Closes the insert-insert create race (the observed bug). The residual update-update race (two handlers updating an existing row) is deferred as OQ-3 — never the observed failure, and closing it would mean
UpdateRevisionacross four handler files. Tracked alongside the analogous Operations BC read-model pattern.Tests
AppHostMessagePack advisory).Prompt + retrospective included:
docs/prompts/implementations/M9-S7-…anddocs/retrospectives/M9-S7-….