Repository navigation
Fix false ConcurrencyException from non-RETURNING event ops in batched SaveChanges (#4778) - #4784
Merged
Merged
Conversation
…iteEvent with a RETURNING OCC upsert
OperationPage.ApplyCallbacksAsync walks one result-set per operation that is not NoDataReturnedCall. Operations that issue non-RETURNING DML (plain UPDATE/DELETE/ INSERT) produce no result-set under NpgsqlBatch, so when such an op is not marked its spurious NextResultAsync() consumes a result-set belonging to a later RETURNING operation, exhausting that operation's reader and raising a false ConcurrencyException. OverwriteEventOperation was the reported trigger (mixed with an optimistic-concurrency projection upsert in one SaveChanges). Audited the whole event-operation family and marked every op that produces no result-set and has a no-op PostprocessAsync: - OverwriteEventOperation, ReplaceEventOperation (masking) - AssignTagWhereOperation, AssignTagWhereHstoreOperation, SetEventTagsHstoreOperation, SetEventTagsHstoreByEventIdOperation, InsertEventTagOperation, InsertEventTagByEventIdOperation (tagging) - IncrementStreamVersionById, IncrementStreamVersionByKey - DeleteProjectionProgress, DeleteEventsOperation (stream compaction) Deliberately not marked: DcbTagVersionAssertion (reads the reader), and the RETURNING upserts/asserts (correctly walked). Closes #4778
This was referenced Jun 26, 2026
This was referenced Jul 9, 2026
This was referenced Aug 3, 2026
This was referenced Aug 10, 2026
This was referenced Sep 17, 2026
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.
Closes #4778.
Problem
Mixing an
OverwriteEvent(a plainUPDATE … mt_events, noRETURNING) with aRETURNINGupsert (e.g. an optimistic-concurrency projection document) in a singleSaveChangesAsyncthrows a falseConcurrencyException, even though nothing actually conflicted. Regressed on 9.x when the closed-shape upsert (#4659) switched to inlineINSERT … ON CONFLICT … RETURNING.Root cause
OperationPage.ApplyCallbacksAsyncwalks one result-set per operation that is notNoDataReturnedCall(Postprocess→NextResultAsync). UnderNpgsqlBatch, only row-producing statements (… RETURNING,SELECT) yield result-sets — plainUPDATE/DELETE/INSERTproduce none.OverwriteEventOperationissues such a non-RETURNINGUPDATE, has a no-opPostprocessAsync, and was not markedNoDataReturnedCall, so its spuriousNextResultAsync()consumed the next operation's result-set. By the time theRETURNINGupsert ran, the reader was exhausted →ReadAsync()returnedfalse→ falseConcurrencyException. (Deletionwas already marked and correctly skipped — the fix already existed for a sibling.)Order matters: the overwrites must land before the
RETURNINGop. They do —OverwriteEventqueues its op immediately whileStorebuilds its upsert duringSaveChanges, and with no doc-type FK relationship the batch stays in insertion order.Fix
Marked every event-family operation that produces no result-set and has a no-op
PostprocessAsyncasNoDataReturnedCall, so the walk skips them (audited the whole family, not just the reported op):OverwriteEventOperation,ReplaceEventOperation(masking)AssignTagWhereOperation,AssignTagWhereHstoreOperation,SetEventTagsHstoreOperation,SetEventTagsHstoreByEventIdOperation,InsertEventTagOperation,InsertEventTagByEventIdOperation(tagging — allINSERT … [SELECT] … ON CONFLICT, noRETURNING)IncrementStreamVersionById,IncrementStreamVersionByKeyDeleteProjectionProgress,DeleteEventsOperation(stream compaction)Deliberately not marked:
DcbTagVersionAssertion(it reads the reader), and theRETURNINGupserts/asserts (correctly walked).Tests
Bug_4778_overwrite_event_with_returning_upsert— the regression test (committed first as a failing repro; throwsJasperFx.ConcurrencyExceptionbefore the fix, passes after).Note
The deeper alternative is to make
ApplyCallbacksAsyncresult-set-driven rather than operation-driven (advance only for ops that actually return data), which would immunize all operation families permanently. This PR takes the targeted marker fix for the event family per the issue; happy to follow up with the reader-driven walk if preferred.