Skip to content

[#33444] DocDB: Expose the unique-index verification scan as a tserver admin RPC - #33602

Open
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-4a-verifier-corefrom
feature-stack/Shopify/uniq-idx-4b-verifier-rpc
Open

[#33444] DocDB: Expose the unique-index verification scan as a tserver admin RPC#33602
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-4a-verifier-corefrom
feature-stack/Shopify/uniq-idx-4b-verifier-rpc

Conversation

@jmeehan16

@jmeehan16 jmeehan16 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wires the verifier core (#33444) to the world: VerifyUniqueIndexTablet on the tablet server
admin service, read-only and paginated, with the preconditions the scan's correctness rests
on enforced at the entry point. No production caller yet -- the verification coordinator
drives it in the next part; end-to-end tests drive it directly.

The RPC (leader-only, BackfillIndex-style pagination):

  • Discharges the applied-intents precondition in two steps: waits for the tablet's safe time
    to pass verify_upper_ht (leader lease held; no future write can land at or below the
    bound), then TransactionParticipant::ResolveIntents(verify_upper_ht) -- safe time alone
    does not imply applied, since transactional apply is asynchronous, and a
    committed-but-unapplied foreground write is invisible to a regular-DB-only scan (the record
    missed could be exactly the foreground half of a duplicate).
  • Validates the active ordering generation against the request's expectation (index table,
    and base_op_index when set -- the coordinator always sets the table; omitting the base
    relaxes to "any active generation for this index table", which is also what the coordinator
    wants: bases are per-tablet activation-op Raft indexes). A released or replaced generation
    fails the RPC without scanning: the window no longer describes the tablet's marked writes.
  • Refuses a window that begins below the applied history cutoff, read from the regular DB's
    flushed frontier -- the cutoff past compactions actually used. Deliberately not the
    retention policy's directive: GetRetentionDirective mutates committed cutoff state
    (unacceptable on a read path) and only bounds future compactions, which the retention-hold
    part of this feature prevents from advancing anyway. Until that hold lands the pre-scan
    check is advisory, so the applied cutoff is re-checked after the scan: a compaction
    advancing it mid-scan fails the call rather than returning a result computed over
    possibly-incomplete history.
  • Rejects an invalid or inverted window at the trust boundary. This matters more than it
    looks: an unset backfill_read_ht decodes to the invalid sentinel, which compares
    greater than any real hybrid time -- both history-cutoff checks would pass vacuously and
    an invalid scan lower bound would reach the verifier.
  • Returns typed error codes for deterministic failures (generation mismatch, non-unique-index
    tablet, invalid window, cutoff violations) so callers do not burn retry budgets on requests
    that cannot succeed.
  • Holds the RocksDB shutdown guard for the scan's duration (the not-blocking variant: tablet
    shutdown aborts the scan, never the reverse).

Tablet::VerifyUniqueIndex resolves the scan options from the tablet: the primary table's
ybidxbasectid column, the tablet metadata as the schema-packing provider, the regular DB
within the tablet's key bounds, and the runtime flag
unique_index_verify_max_buffered_versions_per_group.

Two TEST flags support the stack's end-to-end tests:
TEST_block_index_backfill_ordering_generation_release (master) holds the terminal funnel's
release open so tests can verify an index built by the real backfill path while its
generation is still active, and TEST_pause_verify_unique_index_tablet_rpc (tserver) rejects
verification RPCs with a retryable error to hold a phase open (used by the coordinator part's
failover test).

GenerationMismatchFailsCleanly exercises the generation-reference validation directly
through the RPC (wrong index table, wrong base), via a request-mutation hook on the test
helper.

The e2e suite closes the encoding-drift loop the verifier-core review required: every
physical shape asserted by the unit tests is re-asserted against tablets written by the
production SKIP_ALL backfill and live pg DML, including the design document's named
PK-update-across-the-backfill-boundary regression under both physical shapes.

Stacked on #33403, #33404, #33484, #33544, #33553, #33580, #33584, and #33601,
based on feature-stack/Shopify/uniq-idx-4a-verifier-core 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.

Upgrade/Rollback safety

One wire surface changes, additively: tserver_admin.proto gains the
VerifyUniqueIndexTablet RPC and its request/response messages (fresh messages; no existing
field numbers touched). A new master calling an old tserver would get a clean unknown-RPC
error -- but no production caller exists until the coordinator part, and that caller is
flag-gated and fails open (records INCONCLUSIVE), so mixed-version clusters cannot be
affected. Rollback is trivial: the RPC is read-only, persists nothing, and nothing references
it. As with the rest of the stack: ships in the same release as #33553 through activation,
under the same one-release constraint.

Test plan

macOS arm64, release -- all green:

  • New: PgIndexBackfillVerifier.CleanBuildWithForegroundDml/0 -- funnel build (4-tablet
    source, INCLUDE column) plus post-build inserts, delete + re-insert of the same indexed
    value from a different base row, and an INCLUDE-only update: CLEAN, groups scanned > 0,
    no resume key.
  • New: PgIndexBackfillVerifier.PkUpdateAcrossBackfillBoundaryIsClean/0 -- the design
    document's named regression, standalone-ybidxbasectid shape (default flags): CLEAN.
  • New: PgIndexBackfillVerifierPackedUpdate.PkUpdateAcrossBackfillBoundaryIsClean/0 --
    the same regression in the packed shape (packed_row + pack_full_row_update + mark):
    CLEAN.
  • New: PgIndexBackfillVerifier.DetectsPreexistingDuplicates/0 -- duplicates that
    pre-existed the SKIP_ALL build (which therefore succeeded): VIOLATION with a value-free
    reason.
  • New: PgIndexBackfillVerifier.GenerationMismatchFailsCleanly/0 -- wrong index table id
    and wrong generation base each fail without scanning.
  • New: PgIndexBackfillVerifierReleased.VerifyAfterReleaseFailsCleanly/0 -- with the
    funnel release NOT held open, a late verification RPC fails cleanly on the released
    generation.
  • Regressions: unique_index_verifier-test (18), the skip_all e2e suite through the real
    activation flow, OrderingGenerationChangeMetadataOp tablet_peer test.

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

At this PR's own commit (68db79ae25):

  • Debug: all six of this PR's e2e tests, both params (12 runs) -- including the
    PK-update-across-backfill-boundary cases in both physical shapes, the
    pre-existing-duplicates positive control, and GenerationMismatchFailsCleanly (which
    the rework moved into this layer).
  • Debug: unique_index_verifier-test, tablet_peer-test.

Integrated at the stack tip (c69686cd0d, which contains this commit):

  • TSAN/ASAN/debug: tablet_peer-test, unique_index_verifier-test,
    fixed_hybrid_time_write_id-itest.
  • ASAN + debug + release: full pg_index_backfill-test.
    The only debug-build pg_index_backfill-test failure in the re-validation is
    PgIndexBackfillBackendsManager.NoAbortTxn/1 (GetSafeTime 60s timeout) -- the pre-
    existing upstream flake that fails 3/3 at a base commit carrying none of this stack's
    code (upstream hardening lineage [YSQL] test: harden YSQL backends manager tests #19351).

@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy Preview for infallible-bardeen-164bc9 ready!

Name Link
🔨 Latest commit 15a71ee
🔍 Latest deploy log https://app.netlify.com/projects/infallible-bardeen-164bc9/deploys/6a91fb8a1ee5870008aab55b
😎 Deploy Preview https://deploy-preview-33602--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 31, 2026 15:18
…r admin RPC

Wires the verifier core to the world: VerifyUniqueIndexTablet on the tablet
server admin service, read-only and paginated, with the preconditions the
scan's correctness rests on enforced at the entry point. No production caller
yet -- the verification coordinator drives it in a later part; end-to-end
tests drive it directly.

The RPC (leader-only, BackfillIndex-style pagination):

- Discharges the applied-intents precondition in two steps: waits for the
  tablet's safe time to pass verify_upper_ht (leader lease held; no future
  write can land at or below the bound), then
  TransactionParticipant::ResolveIntents(verify_upper_ht) -- safe time alone
  does not imply applied, since transactional apply is asynchronous, and a
  committed-but-unapplied foreground write is invisible to a regular-DB-only
  scan (the record missed could be exactly the foreground half of a
  duplicate).
- Validates the active ordering generation against the request's expectation
  (index table, and base_op_index when set -- the coordinator always sets it;
  omitting it relaxes to "any active generation for this table" for test
  tooling). A released or replaced generation fails the RPC without scanning:
  the window no longer describes the tablet's marked writes.
- Refuses a window that begins below the *applied* history cutoff, read from
  the regular DB's flushed frontier -- the cutoff past compactions actually
  used. Deliberately not the retention policy's directive: GetRetentionDirective
  mutates committed cutoff state (unacceptable on a read path) and only bounds
  future compactions, which the retention-hold part of this feature prevents
  from advancing anyway. Until that hold lands the pre-scan check is advisory,
  so the applied cutoff is re-checked after the scan: a compaction advancing
  it mid-scan fails the call rather than returning a result computed over
  possibly-incomplete history.

Deterministic failures (generation mismatch, non-unique-index tablet, invalid or
inverted window) return typed error codes, so callers do not burn a retry budget
on requests that cannot succeed. The scan holds the RocksDB shutdown guard for
its duration (the not-blocking variant: tablet shutdown aborts the scan, never
the reverse), and an unset backfill_read_ht is rejected at the boundary -- it
decodes to the invalid sentinel, which compares greater than any real hybrid
time and would make the history-cutoff checks pass vacuously.

Tablet::VerifyUniqueIndex resolves the scan options from the tablet: the
primary table's ybidxbasectid column, the tablet metadata as the
schema-packing provider, the regular DB within the tablet's key bounds, and
the new runtime flag unique_index_verify_max_buffered_versions_per_group.

TEST_block_index_backfill_ordering_generation_release (master) holds the
terminal funnel's release open so tests can verify an index built by the real
backfill path while its generation is still active.

The e2e suite closes the encoding-drift loop the verifier-core review
required: every physical shape asserted by the unit tests is re-asserted
against tablets written by the production SKIP_ALL backfill and live pg DML.

Test Plan:
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillVerifier.CleanBuildWithForegroundDml/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillVerifier.PkUpdateAcrossBackfillBoundaryIsClean/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillVerifier.DetectsPreexistingDuplicates/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillVerifierPackedUpdate.PkUpdateAcrossBackfillBoundaryIsClean/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillVerifierReleased.VerifyAfterReleaseFailsCleanly/0'
The PK-update regression runs under both physical shapes (standalone
ybidxbasectid column with default flags; packed V2 + kIsUpdateFlag with
pack_full_row_update + mark) and must be Clean in both.
Regressions: unique_index_verifier-test (18), OrderingGenerationActivatedAndReleased
(release still fires with the TEST flag off), OrderingGenerationChangeMetadataOp
tablet_peer test -- all green.

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