Skip to content

Stop expiring tombstones and make delete paths consistent - #99

Merged
bbrks merged 8 commits into
mainfrom
tombstone-expiry
Oct 8, 2026
Merged

bbrks merged 8 commits into
mainfrom
tombstone-expiry

Conversation

@torcolvin

Copy link
Copy Markdown
Collaborator

Broken:

  • The expirer selected every row with an expired exp, including tombstones. A tombstone that kept an expiry was deleted again, which made a second deletion event with a new CAS and a higher revSeqNo. Couchbase Server does not expire tombstones. This could end up causing spinning in the expiry va;lue.
  • DeleteWithXattrs did not set tombstone=1 or isJSON=0, and kept the old expiry. Couchbase Server sets the expiry of the tombstone to 0.
  • WriteCas with a nil value did not set tombstone=1 and kept user xattrs. Delete removes user xattrs.

Fixed:

  • expireDocuments and nextExpiration skip rows with tombstone=1.
  • DeleteWithXattrs sets exp=0, isJSON=0 and tombstone=1, the same values that remove() writes.
  • WriteCas with a nil value sets tombstone=1 and removes user xattrs.
  • remove() uses processXattrs with removeUserXattrs in place of its own loop.
  • TestExpirationSkipsTombstones covers each delete path. It checks the tombstone flag and exp, and that the expirer does not change the tombstone. TestTombstoneRemovesUserXattrs checks that Delete and WriteCas with a nil value keep system xattrs and remove user xattrs.

This doesn't fail any tests currently in Sync Gateway but it can fail some tests in the future. I didn't want to do a test in Sync Gateway because I can't use synctest - gocb is not compatible.

Broken:

- The expirer selected every row with an expired exp, including
  tombstones. A tombstone that kept an expiry was deleted again, which
  made a second deletion event with a new CAS and a higher revSeqNo.
  Couchbase Server does not expire tombstones.
- DeleteWithXattrs did not set tombstone=1 or isJSON=0, and kept the
  old expiry. Couchbase Server sets the expiry of the tombstone to 0.
- WriteCas with a nil value did not set tombstone=1 and kept user
  xattrs. Delete removes user xattrs.

Fixed:
- expireDocuments and nextExpiration skip rows with tombstone=1.
- DeleteWithXattrs sets exp=0, isJSON=0 and tombstone=1, the same
  values that remove() writes.
- WriteCas with a nil value sets tombstone=1 and removes user xattrs.
- remove() uses processXattrs with removeUserXattrs in place of its own
  loop.
- TestExpirationSkipsTombstones covers each delete path. It checks the
  tombstone flag and exp, and that the expirer does not change the
  tombstone. TestTombstoneRemovesUserXattrs checks that Delete and
  WriteCas with a nil value keep system xattrs and remove user xattrs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved issues remain in WriteCas nil-value handling across write branches and tombstone xattr preservation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

This pull request prevents tombstones from being re-expired and aligns delete metadata and xattr handling with Couchbase behavior.

Changes:

  • Excludes tombstones from expiration scans and scheduling.
  • Normalizes tombstone metadata for delete paths.
  • Adds regression coverage for expiration and xattr cleanup.
File Summary
collection+xattrs.go Updates DeleteWithXattrs tombstone metadata.
collection.go Adjusts nil writes, xattr cleanup, and expiration filtering. Review findings identify moderate and critical issues in nil-write branches and tombstone xattr preservation.
collection_test.go Tests user and system xattr behavior.
bucket_test.go Tests expiration behavior across delete paths.
bucket_api.go Excludes tombstones from next-expiration calculation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread collection.go Outdated
Comment thread collection.go Outdated
torcolvin and others added 2 commits September 25, 2026 12:01
…ranch

- The insert, upsert and append branches of WriteCas did not set
  tombstone=1 for a nil value, so the row had no body but counted as live.
- A nil WriteCas over an existing tombstone cleared all xattrs, because the
  xattr reset checked the old tombstone flag. Xattrs are now cleared only
  when a non-nil value brings a tombstone back.
- TestWriteCasNilSetsTombstone and
  TestWriteCasNilOnTombstoneKeepsSystemXattrs cover these cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Add and Set brought a tombstone back but left tombstone=1. Add also
  did not increment revSeqNo and always reported revSeqNo 1 in the event.
- SetXattrs, UpdateXattrs and WriteWithXattrs with a nil body on a
  tombstone set tombstone=0 on a row with no body.
- Add no longer emits a DCP event when the key exists and nothing is
  written.
- TestTombstoneFlagMatchesBody covers each of these writes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread collection_test.go
Comment thread collection+xattrs.go Outdated
Comment thread collection.go Outdated
Comment thread collection.go Outdated
torcolvin and others added 5 commits October 6, 2026 15:13
Set, SetRaw, Add and AddRaw always write a live document, and an xattr-only write keeps the document's existing tombstone state. _set reads the tombstone column instead of inferring it from a nil value.
Couchbase Server never creates a tombstone for a nil WriteCas value. An untyped nil writes null and a nil []byte writes an empty body. WriteCas also reads revSeqNo for a CAS of 0, so a write over a tombstone increases it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bbrks
bbrks merged commit e6f0058 into main Oct 8, 2026
12 checks passed
@bbrks
bbrks deleted the tombstone-expiry branch October 8, 2026 10:07
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.

3 participants