Skip to content

[#33444] DocDB: Add the identity-aware unique-index verification scan - #33601

Open
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-3b-ii-generation-orchestrationfrom
feature-stack/Shopify/uniq-idx-4a-verifier-core
Open

[#33444] DocDB: Add the identity-aware unique-index verification scan#33601
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-3b-ii-generation-orchestrationfrom
feature-stack/Shopify/uniq-idx-4a-verifier-core

Conversation

@jmeehan16

@jmeehan16 jmeehan16 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

The read half of deferred uniqueness verification (#33444): docdb::VerifyUniqueIndexTablet
scans a unique-index tablet's regular database over the inclusive window
[backfill_read_ht, verify_upper_ht] and replays every DocKey group's physical history
chronologically, deciding Clean / Violation / Inconclusive per the design document's
live-identity state machine. Read-only library + unit tests; the tserver RPC and the master
coordinator that drive it are the next parts of the stack.

The window starts each group from an empty live set because SKIP_ALL backfill unconditionally
re-materializes every identity live at backfill_read_ht at backfill_read_ht -- stated as
a precondition on the options, since any change reintroducing conditional backfill writes
invalidates the scan range.

Replay order is hybrid time ascending, write ID ascending within one hybrid time -- which
orders foreground records (write IDs below the marked-domain floor, #33553) before floored
backfill records at an equal hybrid time. Storage order within a group is
subkey-section-major (all row-level versions, then each column's), not hybrid-time-major, so
the default path buffers a group and sorts it chronologically; a group exceeding the buffer
bound is replayed by a bounded-memory merge of three reverse section cursors (row-level,
liveness column, ybidxbasectid column -- other columns never affect identity state). Within
one section a Prev() walk is natively chronological.

Physical records assemble into logical events per (DocKey, hybrid time): packed rows and
row-level tombstones are self-contained events at their own write IDs (packed V2 rows with
kIsUpdateFlag are updates; V1 and unflagged V2 are inserts). Non-packed column records group
by write-ID structure: marked records (at or above kBackfillWriteIdFloor) group by exact
write ID -- every record of one marked backfill operation shares the operation's Raft-index
write ID, so two non-packed backfill candidates surface as two inserts (a Violation) even on
packing-disabled clusters -- while unmarked records assemble into one foreground event
(intents take consecutive write IDs; two foreground writers of one DocKey at one commit
hybrid time would collide physically, and conflicting shapes fail closed). With the liveness
column present a group is an insert; a lone ybidxbasectid record is the in-place identity
update yb_lsm.c emits for PK updates; other-column-only writes (INCLUDE updates) change
nothing. The identity is the decoded ybidxbasectid value, comparable across packed and
non-packed shapes. Ambiguous or unknown encodings return Inconclusive, never a guess.

State machine: Insert adds (second distinct live identity = Violation; repeated identity =
idempotent retry -- marked writes make chunk retries physically visible as same-identity
versions); Update replaces, or establishes on an empty set (it cannot mask a duplicate: two
window-start identities would both have been re-materialized as inserts); Delete clears the
whole set (unique-index tombstones are DocKey-level).

Violation/Inconclusive results carry value-free reasons and counts only; the keyed diagnostic
fingerprint is deliberately deferred to the observability part, where key management belongs.
Pagination is DocKey-aligned: the scan stops on group budget or deadline with an encoded
resume key; a group's history is never split across pages.

One activation-decision input recorded for the eventual default-on work: packed V1
full-row updates carry no flags byte and are indistinguishable from inserts, so verify-time
correctness requires ysql_mark_update_packed_row whenever pack_full_row_update is
enabled.

The scan's iterators set fill_cache = false: a one-shot pass over history that regular
reads never touch should not evict the working set.

No production caller and no daemon changes: pure library + tests, no performance surface.

Stacked on #33403, #33404, #33484, #33544, #33553, #33580, and #33584, based on
feature-stack/Shopify/uniq-idx-3b-ii-generation-orchestration per the feature-stack
workflow, so the diff is exactly this PR's own commit. A failing pr-title check on stacked
PRs is a known gap in that check; the stack merges bottom-up and retargets as parents merge.

Test plan

macOS arm64, release -- all green:

  • unique_index_verifier-test, 18 cases: empty tablet; single backfill insert; the
    funnel-duplicate shape (two distinct identities at backfill_read_ht under distinct
    marked write IDs) in both the packed and the non-packed (packing-disabled) shapes, with
    the same-identity non-packed variant clean; idempotent retried chunks;
    foreground-before-backfill at an equal hybrid time (same identity clean, distinct
    identities violation); insert-delete-insert clean; transient overlap violation;
    in-place PK update (standalone column shape); packed-V2-with-flag PK update (and
    unflagged packed insert still violating); packed V1 shapes;
    update-establishes-on-empty; INCLUDE-column update preserving the live identity;
    below/above-window records ignored; unknown encoding inconclusive with reason;
    DocKey-aligned pagination with resume; oversized-group fallback (violation found in a
    later group; clean oversized group replayed in correct chronological order).

AlmaLinux (x86_64, clang21) -- all green, re-validated after the stack-wide rework:

  • Debug: unique_index_verifier-test at this PR's own commit (4dd45c0827).
  • TSAN/ASAN/debug: unique_index_verifier-test at the stack tip (c69686cd0d), which
    contains this commit; also green under release.

@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy Preview for infallible-bardeen-164bc9 ready!

Name Link
🔨 Latest commit c09182b
🔍 Latest deploy log https://app.netlify.com/projects/infallible-bardeen-164bc9/deploys/6a91fb8b312ae5000897a043
😎 Deploy Preview https://deploy-preview-33601--infallible-bardeen-164bc9.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@jmeehan16
jmeehan16 marked this pull request as ready for review August 28, 2026 21:39
The read half of deferred uniqueness verification: a library that scans a
unique-index tablet's regular database over the inclusive window
[backfill_read_ht, verify_upper_ht] and replays every DocKey group's physical
history chronologically, deciding Clean / Violation / Inconclusive per the
design document's live-identity state machine. Read-only; no production
caller yet (the tserver RPC and master integration follow).

The window starts each group from an empty live set because SKIP_ALL backfill
unconditionally re-materializes every identity live at backfill_read_ht *at*
backfill_read_ht -- stated as a precondition on the options, since any change
reintroducing conditional backfill writes invalidates the scan range.

Replay order is hybrid time ascending, write ID ascending within one hybrid
time -- which orders foreground records (write IDs below the marked-domain
floor) before floored backfill records at an equal hybrid time. Storage order
within a group is subkey-section-major (all row-level versions, then each
column's), not hybrid-time-major, so the default path buffers a group and
sorts it chronologically; a group exceeding the buffer bound is replayed by a
bounded-memory merge of three reverse section cursors (row-level, liveness
column, ybidxbasectid column -- other columns never affect identity state).
Within one section a Prev() walk is natively chronological.

Physical records assemble into logical events per (DocKey, hybrid time):
packed rows and row-level tombstones are self-contained events at their own
write IDs (packed V2 rows with kIsUpdateFlag are updates; V1 and unflagged V2
are inserts). Non-packed column records group by write-ID structure: marked
records (at or above kBackfillWriteIdFloor) group by exact write ID -- every
record of one marked backfill operation shares the operation's Raft-index
write ID, so two non-packed backfill candidates surface as two inserts (a
Violation) even on packing-disabled clusters -- while unmarked records
assemble into one foreground event (intents take consecutive write IDs; two
foreground writers of one DocKey at one commit hybrid time would collide
physically, and conflicting shapes fail closed). With the liveness column
present a group is an insert; a lone ybidxbasectid record is the in-place
identity update yb_lsm.c emits for PK updates; other-column-only writes
(INCLUDE updates) change nothing. The identity is the decoded ybidxbasectid value, comparable
across packed and non-packed shapes. Ambiguous or unknown encodings return
Inconclusive, never a guess.

State machine: Insert adds (second distinct live identity = Violation;
repeated identity = idempotent retry -- marked writes make chunk retries
physically visible as same-identity versions); Update replaces, or
establishes on an empty set (it cannot mask a duplicate: two window-start
identities would both have been re-materialized as inserts); Delete clears
the whole set (unique-index tombstones are DocKey-level).

Violation/Inconclusive results carry value-free reasons and counts only; the
keyed diagnostic fingerprint is deliberately deferred to the observability
part, where key management belongs.

Pagination is DocKey-aligned: the scan stops on group budget or deadline with
an encoded resume key; a group's history is never split across pages.

Test Plan:
./yb_build.sh release --cxx-test unique_index_verifier-test
18 cases: empty tablet; single backfill insert; the funnel-duplicate shape
(two distinct identities at backfill_read_ht under distinct marked write
IDs) in both the packed and the non-packed (packing-disabled) shapes, with
the same-identity non-packed variant clean; idempotent retried chunks; foreground-before-backfill at an equal
hybrid time (same identity clean, distinct identities violation);
insert-delete-insert clean; transient overlap violation; in-place PK update
(standalone column shape); packed-V2-with-flag PK update (and unflagged
packed insert still violating); packed V1 shapes; update-establishes-on-empty;
INCLUDE-column update preserving the live identity; below/above-window
records ignored; unknown encoding inconclusive with reason; DocKey-aligned
pagination with resume; oversized-group fallback (violation found in a later
group, clean oversized group replayed in correct chronological order).

Assisted-By: devx/08801cf6-2854-4c6f-a378-1d98d239b22d

---
_automated · Claude Fable 5 (opencode)_
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.

2 participants