Skip to content

CBG-5764: keep _mou previous values describing one mutation - #8685

Draft
torcolvin wants to merge 1 commit into
mainfrom
CBG-5764
Draft

CBG-5764: keep _mou previous values describing one mutation#8685
torcolvin wants to merge 1 commit into
mainfrom
CBG-5764

Conversation

@torcolvin

@torcolvin torcolvin commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Base of a four-PR stack for CBG-5764.

_mou.pCas and _mou.pRev have to describe the same mutation - the write that last changed the document body. When a metadata-only write replaced a mutation that was itself metadata-only, pRev was refreshed to the current revSeqNo while pCas was carried forward, so the two pointed at different mutations.

  • computeMetadataOnlyUpdate now carries both previous values forward together when the mutation being replaced was itself a metadata-only update.
  • correctVersionAheadOfCAS/restampVersionCAS re-stamp the CAS of a write whose generated version ran ahead of it, reading $document.revid under the CAS guard so pRev names the write actually being corrected.
  • Tests cover the live and tombstone cases and a second metadata-only write from a different path, checking that both previous values survive it.

Verified on rosmar and against a real Couchbase Server.

Stack

  1. this PR - keep _mou previous values describing one mutation
  2. CBG-5764: move SafeDocumentName to testing/sgtest #8686 - move SafeDocumentName to testing/sgtest
  3. CBG-5764: add WriteUpdateXattrOnly, an xattr-only read-modify-write #8687 - add WriteUpdateXattrOnly
  4. CBG-5764: never write a _mou whose pRev is a made-up zero #8688 - never write a _mou whose pRev is a made-up zero

🤖 Generated with Claude Code

_mou records that Sync Gateway changed a document's metadata without
touching its body: _mou.cas names the mutation just made, and pCas/pRev
name the mutation that last wrote the body. pRev did not keep up with
pCas:

- computeMetadataOnlyUpdate carried pCas forward when replacing a
  metadata-only update but always wrote the document's current revision
  sequence number, so chained metadata-only updates left pCas naming the
  body write and pRev naming the update before them.

- restampVersionCAS wrote pRev as 0, because updateAndReturnDoc reads
  the document without the revSeqNo virtual xattr and so never populates
  doc.RevSeqNo in the write path. The re-stamp is now a read-modify-write
  that takes the revision sequence number the server assigned the write
  it is correcting, declining the correction when the document has moved
  on since - previously a CAS mismatch, now ErrUpdateCancel. Both mean
  the write being corrected has been superseded, so both are skipped,
  behind a shared isSupersededWriteError predicate.

- MigrateAttachmentMetadata hand-built its _mou and never read the
  existing one, so it could not carry anything forward. It now uses
  computeMetadataOnlyUpdate like the other metadata-only writers.

Only pRev was ever wrong. Running the tests below against the parent
commit places the damage, where a chained update is one landing on a
document whose previous mutation was already a metadata-only update:

| metadata-only write path      | pRev, first update | pRev, chained |
|-------------------------------|--------------------|---------------|
| on-demand import for get      | correct            | wrong         |
| on-demand import for write    | correct            | wrong         |
| attachment metadata migration | correct            | wrong         |
| channel history compaction    | correct            | wrong         |
| resync                        | correct            | wrong         |
| version-CAS correction        | wrong, always 0    | wrong         |

Nothing else about these paths was wrong:

| behaviour                              | before this commit |
|----------------------------------------|--------------------|
| _mou.cas naming the mutation just made | correct            |
| pCas, chained updates included         | correct            |
| re-stamping a tombstone                | already worked     |
| declining a superseded re-stamp        | already worked     |

Re-stamping a tombstone already worked because updateXattrs sets
SubdocDocFlagAccessDeleted, and WriteTombstoneWithXattrs reaches that
same call for an existing tombstone. A re-stamp superseded by a
concurrent write was already declined, on a CAS mismatch.

TestMetadataOnlyUpdateWritePaths covers the paths whose metadata-only
write lands on a document some earlier write left behind - both
on-demand import paths, attachment metadata migration, channel history
compaction and resync - asserting that _mou.cas names the mutation just
made, that pCas and pRev name the last write to the body, and that both
survive a following metadata-only write from a different path.

TestRestampVersionCASMou covers the version-CAS correction separately,
over a live document and a tombstone. It is the one path whose
metadata-only write follows a body write it makes itself, so _mou names
that write rather than anything observable before it, and both previous
values are pinned by sandwiching them between the mutation preceding the
write under test and the re-stamp that follows it. Channel history
compaction stands in for resync as the second metadata-only write there,
as resync declines a tombstone.

TestAttachmentMigrationMouCarriedForward covers migration of a document
whose previous mutation was already a metadata-only update, as arrives
by mobile XDCR from a cluster that had not migrated its attachment
metadata. TestRestampVersionCASSkipsConcurrentWrite covers the declined
correction, asserting that the re-stamp is refused in a way
correctVersionAheadOfCAS skips on and that the concurrent write is left
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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