Skip to content

fix: leave a write of a document the same save inserts unguarded by the concurrency token - #92

Open
kerem-acer wants to merge 1 commit into
mainfrom
fix/concurrency-token-same-save-insert
Open

kerem-acer wants to merge 1 commit into
mainfrom
fix/concurrency-token-same-save-insert

Conversation

@kerem-acer

Copy link
Copy Markdown
Contributor

Adding a document and then replacing, updating or deleting it in the same save fails with ConcurrencyException under the concurrency token. A delete counts here when soft delete turns it into an update. The whole save rolls back. InsurUp core hits this in its policy import. A policy created and endorsed in one file batch is Added and then Replaced before the batch saves, and core works around it with Without(ConcurrencyTokenFeature.Key) (core#1790).

The cause: ConcurrencyTokenInterceptor.SavingAsync guards the replace and increments the token on the document before any write runs. The insert then writes the document with the incremented token. The replace, checked against the token as it was read, matches nothing.

What it does

SavingAsync first collects the documents the save inserts, by reference, but only when the save inserts any. A replace, update or delete made with one of those documents is neither checked nor incremented. The insert writes the document as it is, and nothing can have changed it in between. It's stored with the token it was added with, and the next save checks against that. A different instance with the same key is still guarded as before. docs/features.md says this under "Concurrency token".

Tests

ConcurrencyTokenTests.SameSave.cs (integration):

  • Replace_DocumentTheSameSaveInserts_WritesItWithTheTokenAsAdded: the latest state is stored at ver: 0.
  • Update_DocumentTheSameSaveInserts_AppliesTheUpdateAndLeavesTheToken: the update applies and the token stays 0.
  • Delete_SoftDeletableDocumentTheSameSaveInserts_MarksItWithTheTokenAsAdded: the document is stored marked, at Version: 0.
  • Replace_AfterTheSaveThatInsertedAndReplacedTheDocument_IsCheckedAgainstTheStoredToken: the next checked replace succeeds and stores 1.
  • Delete_DocumentTheSameSaveInserts_DeletesIt: a hard delete, covered for completeness.

The first four fail on main with ConcurrencyException; the hard delete passes either way, because a delete doesn't increment. All of them pass here. ConcurrencyTokenTests (31) pass on net10.0 and net11.0, and ChangeTrackingTests (74) and SoftDeleteTests (11) on net10.0. Formatting and the Release /warnaserror rebuild are clean.

🤖 Generated with Claude Code

…he concurrency token

Adding a document and then replacing, updating or deleting it (with soft delete,
an update) in the same save failed with ConcurrencyException. The token guarded
the write and incremented the token on the document before the insert wrote it,
so the insert stored the incremented token and the write, checked against the one
before, matched nothing; the whole save rolled back. InsurUp core's policy import
does exactly this for a policy created and endorsed in one file batch, and works
around it with Without(ConcurrencyTokenFeature.Key).

Such a write is now neither checked nor incremented: the insert writes the
document as it is, nothing can have changed it in between, and it's stored with
the token it was added with, which the next save checks against. A different
instance with the same key is guarded as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant