Skip to content

[#33444] DocDB: Add persisted index-backfill ordering-generation record and tablet fences - #33580

Open
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-3a-marked-write-storagefrom
feature-stack/Shopify/uniq-idx-3b-i-generation-record
Open

[#33444] DocDB: Add persisted index-backfill ordering-generation record and tablet fences#33580
jmeehan16 wants to merge 1 commit into
feature-stack/Shopify/uniq-idx-3a-marked-write-storagefrom
feature-stack/Shopify/uniq-idx-3b-i-generation-record

Conversation

@jmeehan16

Copy link
Copy Markdown
Collaborator

Summary

Deferred uniqueness verification (#33444) scans the marked versions that unique-index
backfill wrote at the fixed backfill hybrid time (#33553). That scan is only sound against a
stable tablet set and a validated write-ID sequence, so each index tablet needs a durable
record of the in-flight backfill's ordering state -- one that survives restart, remote
bootstrap, and inheritance by split children and clones.

The record (IndexBackfillOrderingGenerationPB, superblock field 39, persistence following
the cdc_sdk_safe_time pattern): active, the owning index table_id, base_op_index,
retention_barrier_ht, and write_id_floor_version. The full combined shape is defined
here; retention_barrier_ht is consumed by the verification read fence and
identity/activation by the master orchestration, both in later parts. The durable form of an
inactive generation is field absence, and the setter normalizes to keep in-memory and
durable state identical; LoadFromSuperBlock clears the in-memory record when the field is
absent, so a remote-bootstrap superblock replacement cannot resurrect a released generation.

Enforcement while a generation is active, all before WAL append via the ValidateLeaderOpId
hook (#33553):

  • Write base validation -- marked fixed-hybrid-time writes must carry Raft indexes
    strictly above base_op_index (the activation operation's own index, assigned by the
    master flow in the next part): an index at or below the base cannot belong to this
    generation and would indicate misrouting or replay of a foreign sequence. Rejecting marked
    writes when no generation is active also arrives with the master activation flow.
  • Split fence -- SplitOperation is rejected while active: verification must run
    against a stable tablet set. This is the fail-closed layer under the master-side split
    fence (next part; today's master-side suppression is in-memory only and lost on failover).
    TEST_bypass_index_backfill_ordering_generation_split_fence exists to prove the fence is
    for scan stability and lifecycle tracking, not key correctness: with the fence forced
    open, split children inherit the parent's marked entries, continue drawing write IDs from
    their own Raft indexes, and per-key write-ID uniqueness still holds (tested).
  • Clone fence -- CloneOperation is rejected while active: the clone would inherit an
    active generation with no backfill job attached, permanently fencing its splits and
    pinning its retention.

With no generation active (the only production state until the master orchestration lands),
the hooks reduce to reading a defaulted struct and returning OK for every split, clone, and
marked write.

Stacked on #33403, #33404, #33484, #33544, and #33553, based on
feature-stack/Shopify/uniq-idx-3a-marked-write-storage 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

Proto change: RaftGroupReplicaSuperBlockPB.index_backfill_ordering_generation (new
optional message, field 39). Additive:

  • Rollback with a flushed generation record: old binaries parse the superblock without
    error and ignore the field -- no crash, no load failure. However, the superblock is
    regenerated from in-memory state on every metadata flush (not round-tripped), so the first
    flush on an old binary drops the record; a subsequent re-upgrade would see no generation
    and silently stop enforcing the base validation and the split/clone fences. This is
    production-safe today because nothing can activate a generation: the setter's only callers
    are tests, and the master activation flow (next part) is gated behind the same
    one-release-stack constraint as [#33444] DocDB: Add marked fixed-hybrid-time write path with Raft-index write IDs #33553 -- AutoFlag promotion, which requires all nodes
    upgraded, is what authorizes the first activation.
  • Mixed-version cluster: the record is tablet-local metadata; replicas each persist
    their own copy when the (Raft-replicated) activation operation applies. The only wire path
    for the field itself is a remote-bootstrap superblock copy, where an old-binary recipient
    drops the unknown field on its first flush -- the same benign behavior as rollback. Until
    the activation operation exists, mixed clusters never contain the field at all.

Behavioral surface pre-activation: the split/clone/write hooks run unconditionally on
leaders but return OK with the defaulted (inactive) record, so splits, clones, and writes
are unaffected -- covered by the full tablet-split-itest (TSAN+debug),
clone-tablet-itest (ASAN+debug), remote_bootstrap-itest (ASAN), and a full release
pg_index_backfill-test regression, all green.

No gflag default changes; the new flag is TEST-only.

Test plan

macOS arm64, release -- all green:

  • New: TabletPeerTest.IndexBackfillOrderingGenerationPersistsAcrossReload -- full
    combined record round-trips through the superblock; releasing persists as field
    absence.
  • New: TabletPeerTest.IndexBackfillOrderingGenerationClearedBySuperblockReplacement --
    remote-bootstrap-style ReplaceSuperBlock with an absent field clears a stale local
    generation.
  • New: TabletPeerTest.FixedHybridTimeWriteRejectedAtOrBelowGenerationBase -- marked
    write with index at/below the base rejected before WAL append; index above the base
    admitted; released generation does not constrain.
  • New: TabletPeerTest.SplitRejectedWhileOrderingGenerationActive /
    CloneRejectedWhileOrderingGenerationActive -- both rejected pre-WAL, nothing logged
    or committed.
  • New: FixedHybridTimeWriteIdITest.MasterSplitRefusedWhileOrderingGenerationActive --
    end-to-end through the master split path: split refused via the tablet fence while
    active; releasing the generation lets the same split complete (positive control).
  • New: FixedHybridTimeWriteIdITest.OrderingGenerationSurvivesRemoteBootstrap --
    tombstoned follower is remote-bootstrapped back by the leader; the recovered replica
    carries the generation.
  • New: FixedHybridTimeWriteIdITest.SplitWithFenceBypassedPreservesPerKeyWriteIdUniqueness
    -- fence forced open, tablet split; children inherit the generation and the parent's
    marked entries; a marked rewrite of an inherited key at the same fixed hybrid time
    lands as a second distinct physical version; per-key (key, HT, write ID) uniqueness
    holds on both children.
  • Regression: TabletPeerTest.FixedHybridTimeWritesUseRaftIndexWriteId and the [#33444] DocDB: Add marked fixed-hybrid-time write path with Raft-index write IDs #33553
    suites.

AlmaLinux (x86_64, clang21) -- all green:

  • TSAN: tablet_peer-test, fixed_hybrid_time_write_id-itest,
    non_transactional_batch_writer-test, full tablet-split-itest (the split-fence hook
    runs for every split).
  • ASAN: tablet_peer-test, fixed_hybrid_time_write_id-itest,
    non_transactional_batch_writer-test, full clone-tablet-itest, full
    remote_bootstrap-itest (superblock replacement surface).
  • Debug: tablet_peer-test, fixed_hybrid_time_write_id-itest,
    non_transactional_batch_writer-test, tablet_bootstrap-test, full
    clone-tablet-itest, full tablet-split-itest.
  • Release: full pg_index_backfill-test (181/181) -- inertness regression; this change
    is invisible to the pg layer until the master orchestration lands.

@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy Preview for infallible-bardeen-164bc9 ready!

Name Link
🔨 Latest commit 11ccf5d
🔍 Latest deploy log https://app.netlify.com/projects/infallible-bardeen-164bc9/deploys/6a91fb8b7c86e100086d4ba9
😎 Deploy Preview https://deploy-preview-33580--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.

…rd and tablet fences

Deferred uniqueness verification scans the marked versions that unique-index
backfill wrote at the fixed backfill hybrid time. That scan is only sound
against a stable tablet set and a validated write-ID sequence, so each index
tablet needs a durable record of the in-flight backfill's ordering state --
one that survives restart, remote bootstrap, and inheritance by split
children.

The record (`IndexBackfillOrderingGenerationPB`, superblock field 39,
persistence following the `cdc_sdk_safe_time` pattern): `active`, the owning
index `table_id`, `base_op_index`, `retention_barrier_ht`, and
`write_id_floor_version` (numbering pinned in the proto: 1 = the initial
floor, 0 = absent/unknown). The full combined shape is defined here;
`retention_barrier_ht` is consumed by the verification read fence and
identity/activation by the master orchestration, both in later parts.
The durable form of an inactive generation is field absence:
`LoadFromSuperBlock` clears the in-memory record when the field is absent
(so a remote-bootstrap superblock replacement from a source without a
generation cannot resurrect a released one), and the setter normalizes
inactive input to the default -- the caller contract is "set an active
generation or set {}", keeping in-memory and durable state identical.

While a generation is active on a tablet:

- Marked fixed-hybrid-time writes must carry Raft indexes strictly above
  `base_op_index` (`WriteOperation::ValidateLeaderOpId`). The base is the
  activation operation's own Raft index, so an index at or below it cannot
  belong to this generation -- it would indicate misrouting or replay of a
  foreign sequence. (Rejecting marked writes when *no* generation is active
  arrives with the master activation flow; until then marked writes remain
  production-unreachable.)
- Tablet splitting is rejected before Raft append
  (`SplitOperation::ValidateLeaderOpId`) -- the fail-closed layer under the
  master-side split fence (next part). The master's split path retries and
  succeeds once the generation is released.
- Tablet cloning is rejected the same way. Clone targets build fresh metadata
  (`RaftGroupMetadata::CreateNew`), not a superblock copy, so a clone would
  carry hard-linked snapshot data containing generation-scoped marked write
  IDs with no generation record describing them and no backfill job tracking
  their lifecycle.

`TEST_bypass_index_backfill_ordering_generation_split_fence` forces the
split fence open to demonstrate the property the fence is *not* needed for:
split children inherit the parent's marked entries and continue drawing
write IDs from their own Raft indexes, so per-key write-ID uniqueness holds
even across a split (the fence exists for verification-scan stability and
lifecycle tracking, not key correctness).

Test Plan:
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.FixedHybridTimeWriteRejectedAtOrBelowGenerationBase'
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.IndexBackfillOrderingGenerationPersistsAcrossReload'
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.IndexBackfillOrderingGenerationClearedBySuperblockReplacement'
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.SplitRejectedWhileOrderingGenerationActive'
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.CloneRejectedWhileOrderingGenerationActive'
./yb_build.sh release --cxx-test fixed_hybrid_time_write_id-itest --gtest_filter 'FixedHybridTimeWriteIdITest.MasterSplitRefusedWhileOrderingGenerationActive'
./yb_build.sh release --cxx-test fixed_hybrid_time_write_id-itest --gtest_filter 'FixedHybridTimeWriteIdITest.OrderingGenerationSurvivesRemoteBootstrap'
./yb_build.sh release --cxx-test fixed_hybrid_time_write_id-itest --gtest_filter 'FixedHybridTimeWriteIdITest.SplitWithFenceBypassedPreservesPerKeyWriteIdUniqueness'
Regressions: 3a fixed-hybrid-time tablet_peer tests and leader-change itest,
0a/0b abort tests, PgIndexBackfillSkipAllRaftOrdering e2e -- all green.

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

---
_automated · Claude Fable 5 (opencode)_
@jmeehan16
jmeehan16 force-pushed the feature-stack/Shopify/uniq-idx-3b-i-generation-record branch from 11ccf5d to cc9ba40 Compare August 31, 2026 18:36
jmeehan16 added a commit that referenced this pull request Aug 31, 2026
…fecycle from the master

The persisted ordering generation (#33580) had no production lifecycle: nothing
activated it before marked backfill writes, nothing released it, and the
master-side split suppression protecting a backfill is in-memory only (lost on
failover). This part wires the full lifecycle and flips marked-write gating
fail-closed.

Activation rides ChangeMetadataOperation (a new
`ChangeMetadataRequestPB.index_backfill_ordering_generation` variant, applied
via `Tablet::UpdateIndexBackfillOrderingGeneration` exactly like
`mark_backfill_done`): Raft-replicated, follower-applied, WAL-replayed, and
`base_op_index` is the activation operation's own Raft index, so every replica
and every replay derives the same base with no master-side bookkeeping.
Re-activation (failover resume) is idempotent -- the base only moves up.

Master flow, SKIP_ALL jobs on PGSQL indexed tables only (the mode rides YSQL
chunk requests; YCQL backfill writes are never marked, so generations would
only fence their splits without protecting anything):

- Before the first chunk (`DoBackfill`), the job disables and drains
  index-table splitting, then fans out one waited
  `UpdateOrderingGenerationForTablet` task per index tablet (the
  `GetSafeTimeForTablet` join pattern). All acks -> chunks launch; any failure
  -> the job aborts before a single marked write exists, and CREATE INDEX
  fails cleanly. The drain closes the activation/split TOCTOU: consensus
  already rejects CHANGE_METADATA_OP while a split is pending, the generation
  fence rejects splits after activation applies, and a split appended in the
  append-to-apply window can at worst fail the activation task -- never
  corrupt a scan set that marked writes exist in.
- Release is fire-and-forget from the terminal funnel
  (`UpdateIndexPermissionsForIndexes`), which every success/failure/abort path
  traverses. The tserver metadata validator converges stragglers, mirroring
  the retain_delete_markers machinery: tablets with an active generation join
  its existing GetBackfillStatus poll, and the generation is released locally
  (no Raft) once the master reports a terminal state. `IndexStatusPB` gains
  `BACKFILL_FAILED` (removal-path index permissions map to it) so a failed
  SKIP_ALL job whose funnel release was lost cannot leave an orphaned invalid
  index permanently split-fenced and retention-pinned; the
  retain_delete_markers heal itself stays success-only.
- The tablet-split manager refuses to split an index whose indexed table has a
  durable SKIP_ALL backfill job. The manager's table validation takes no
  catalog locks itself -- the indexed table is a caller-resolved parameter,
  because the manual-split path enters through ValidateSplitCandidateUnlocked
  with the catalog mutex already held (a recursive shared acquisition is fatal
  under lock_debug) (`SysTablesEntryPB.backfill_jobs`, survives
  failover, cleared in the funnel), with an INFO-level skip reason -- today's
  two in-memory suppressions cover only the indexed table and evaporate on
  failover. The tablet-side generation fence stays as the fail-closed layer.

Gating flip: `WriteOperation::ValidateLeaderOpId` now rejects marked writes
with *no* active generation (previously deferred) -- a marked write outside a
generation would store versions nothing tracks or releases (e.g. a stale chunk
retry after the job's terminal state). Tests that drive marked writes directly
now activate a generation first.

`write_id_floor_version` numbering lands as
`kIndexBackfillWriteIdFloorVersion = 1`.

The mode selector remains hardcoded to CHECK_ALL (#33484), so this whole flow
stays production-unreachable; tests drive it through the master-side TEST
override.

Test Plan:
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.OrderingGenerationChangeMetadataOpSetsBaseFromOwnRaftIndex'
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.FixedHybridTimeWriteRejectedAtOrBelowGenerationBase'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillSkipAllRaftOrdering.OrderingGenerationActivatedAndReleased/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillSkipAllActivationFailure.ActivationFailureFailsCreateIndexCleanly/0'
./yb_build.sh release --cxx-test pg_index_backfill-test --gtest_filter 'PgIndexBackfillSkipAllBlocked.SplitFencedDuringBackfill/0'
Retrofitted (marked writes now require an active generation):
./yb_build.sh release --cxx-test tablet_peer-test --gtest_filter 'TabletPeerTest.FixedHybridTimeWrite*'
./yb_build.sh release --cxx-test fixed_hybrid_time_write_id-itest
Regressions: skip_all e2e + marker canary through the real activation flow,
uniq-idx-1 mode tests, DuplicatesExistBeforeBackfill, 3b-i generation tests,
0a/0b abort tests, and drop-path tests for the permissions mapping change
(PgIndexBackfillTest.Drop, PgIndexBackfillFastClientTimeout.DropWhileBackfilling)
-- 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