Repository navigation
fix: match a concurrency token read as zero to a document stored without one - #90
Merged
Merged
Conversation
…out one
A document saved before the token was switched on has no token field. It reads as
zero, but the checked write's { Version: 0 } doesn't match a missing field, so every
replace, update and delete made with it failed the save with ConcurrencyException.
Switching the token on for a collection with documents in it took a backfill, and a
second one after a rolling deploy, for the documents old instances kept inserting.
A token read as zero now matches { Version: { $in: [0, null] } }: a stored zero or no
token at all. The write's increment stores the token from zero, so a later write of
what was read before is checked as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Core wants the concurrency token on most of its aggregates (Proposal, Case, Customer, Policy and others), all of which already have documents in production. A document saved before the token was switched on has no token field. It deserializes with the token at zero, but the checked write's
{ Version: 0 }doesn't match a missing field. So every replace, update and delete made with such a document failed the save withConcurrencyException, including a tracked document's changes.The workaround was a backfill per collection, and it isn't enough on its own. During a rolling deploy, instances still on the old code keep inserting documents without the token after the new code's backfill has run, and those documents can never be saved again. Core's prod deploys overlap old and new instances for about 45 seconds, during which about three proposals a minute are created.
What it does
A token read as zero is matched with
{ Version: { $in: [0, null] } }, which accepts a stored zero or no token at all. Any other value is matched exactly, as before. The write's increment stores the token from zero:$incsets the missing field to 1.A second write made with what was read before therefore still fails the check. Tracked documents need nothing more: their snapshot is the document as deserialized, with the token at zero, so the diff doesn't touch it.
docs/features.mdsays, under "Concurrency token", that switching the token on for a collection with documents takes no migration.Tests
ConcurrencyTokenTests(integration). Each test seeds a contract as raw BSON without the token, as stored before the token was switched on:Replace_DocumentStoredWithoutAToken_WritesAndStoresTheTokenIncrementedFromZero: the replace writes and storesver: 1, and the document's token is 1.Update_DocumentStoredWithoutAToken_AppliesTheUpdateAndStoresTheTokenIncrementedFromZero: the same for an update.SaveAsync_TrackedDocumentStoredWithoutAToken_WritesItsChangesAndStoresTheTokenIncrementedFromZero: the same for a tracked change, with change tracking on.Delete_DocumentStoredWithoutAToken_DeletesIt: the delete removes the document.Replace_DocumentStoredWithoutATokenSavedSinceItWasRead_ThrowsConcurrencyException: two writers both read the document as zero. The first save stores 1, and the second fails withDocumentExists: true.All five fail on
mainand pass here.ConcurrencyTokenTests,ChangeTrackingTests,LoggingTests,TelemetryTestsandQueryableEncryptionTestspass locally on net10.0, andConcurrencyTokenTestson net11.0 too. Formatting and the Release/warnaserrorrebuild are clean.Locally, two unit tests fail on
maintoo, unrelated to this change:VaultCollectionTests.QueryAsync_JoinOnAPagedQuery_IsLeftToTheDriverandQueryAsync_JoinOnAQueryNotFromAVault_IsLeftToTheDrivertime out instead of throwingExpressionNotSupportedException.main's CI passes them, so it's something in the local setup.🤖 Generated with Claude Code