Skip to content

PHOENIX-7874 Eliminate replicating index mutations; regenerate on standby#2566

Open
tkhurana wants to merge 4 commits into
apache:PHOENIX-7562-feature-newfrom
tkhurana:eliminate-index-replication-v2
Open

PHOENIX-7874 Eliminate replicating index mutations; regenerate on standby#2566
tkhurana wants to merge 4 commits into
apache:PHOENIX-7562-feature-newfrom
tkhurana:eliminate-index-replication-v2

Conversation

@tkhurana

@tkhurana tkhurana commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Index tables no longer generate their own replication-log records. The active ships only the data-table record plus a per-(row, ts) PRE_IMAGE (the prior row state observed at lock time), and the standby regenerates every index entry from that record on the replay path.

This targets the PHOENIX-7562-feature-new feature branch (builds on the per-batch coalescing / cell-oriented log format, #2540).

Why

  • Out-of-order replay safety. Because each replicated batch carries the exact prior-row snapshot it was built against, the standby derives each (row, ts) group's next state from its own shipped pre-image instead of a region scan. Shard files can be replayed in any order or in parallel; each group is an independent reproduction of the active preBatchMutate.
  • Cross-cluster rowkeys. CDC/global/local index rowkeys can embed the encoded data-table region name (PARTITION_ID), which differs between clusters. Replicating index cells verbatim would carry the active's region name; the standby instead rebuilds the rowkey with its own.

Main changes

  • Cell-oriented replication format (codec + record + MutationCellGrouper) carrying data cells plus one METAFAMILY pre-image cell per row.
  • IndexRegionObserver: active-side pre-image capture; a forked standby replay path (prepareReplicatedIndexMutations) that groups by (row, ts) and builds index updates from the shipped pre-image.
  • PreImageLocalTable: a LocalHBaseState serving the local-index builder's prior-row-state from the shipped pre-image instead of a region scan; new IndexBuildManager.getIndexUpdates overload accepts it.
  • ReplicationLogProcessor: replay a record as one indivisible batch; do not retry DoNotRetryIOException.

Test coverage

Global, local, uncovered, atomic, conditional-TTL, and CDC (plain and behind an EVENTUAL secondary index) index regeneration; concurrent/out-of-order replay for both global and local indexes; unit tests for (row, ts) grouping isolation and Put/Delete decomposition.

Notes

  • Squashed into a single commit.
  • Please squash-merge.

…ndby

Index tables no longer generate their own replication-log records. Instead the
active ships only the data-table record plus a per-(row, ts) PRE_IMAGE (the
prior row state it observed at lock time), and the standby regenerates every
index entry from that record on the replay path.

This removes the out-of-order replay hazard: because each replicated batch
carries the exact prior-row snapshot it was built against, the standby derives
each (row, ts) group's next state from its own shipped pre-image instead of a
region scan, so shard files can be replayed in any order (or in parallel) and
each group is an independent reproduction of the active preBatchMutate. It also
fixes CDC/global/local index rowkeys that embed the encoded data-table region
name (PARTITION_ID), which differs between clusters -- the standby rebuilds the
rowkey with its own region name.

Main changes:
- Cell-oriented replication log format (codec + record + MutationCellGrouper)
  carrying data cells plus a METAFAMILY pre-image cell per row.
- IndexRegionObserver: active-side pre-image capture; a forked standby replay
  path (prepareReplicatedIndexMutations) that groups by (row, ts) and builds
  index updates from the shipped pre-image.
- PreImageLocalTable: a LocalHBaseState that serves the local-index builder's
  prior-row-state from the shipped pre-image instead of a region scan; new
  IndexBuildManager.getIndexUpdates overload accepts it.
- ReplicationLogProcessor: replay a record as one indivisible batch; do not
  retry DoNotRetryIOException.

Coverage: global, local, uncovered, atomic, conditional-TTL, CDC (plain and
behind an EVENTUAL secondary index), concurrent/out-of-order replay, and unit
tests for (row, ts) grouping isolation and Put/Delete decomposition.
@tkhurana tkhurana requested review from apurtell and virajjasani July 6, 2026 17:34
tkhurana added 3 commits July 6, 2026 15:08
Thread dataTableName into the standby pre-image decode/lookup so a
contract violation names the offending table. decodePreImage and
buildReplicatedRowGroups become instance methods reading the
dataTableName field instead of taking it as a parameter; add a
@VisibleForTesting IndexRegionObserver(String) constructor alongside the
public no-arg one HBase loads reflectively. PreImageLocalTable takes the
table name in its constructor.

Doc notes: decodePreImage javadoc explains the schema-skew case as the
one legitimate missing-pre-image path and why failing loud is correct;
PreImageLocalTable and MutationCellGrouper get class-level thread-safety
notes.
preBatchMutateWithExceptions interleaved the active write path and the standby
replay path in one method behind five scattered isReplication guards, so the
standby path was implicit (defined by what active-only blocks skip). That
fragility surfaced a real bug: getCurrentRowStates was gated only on index
flags, not isReplication, so the standby scanned the data-table region under
lock for prior row state -- the out-of-order-unsafe read the per-row PRE_IMAGE
exists to replace -- and then discarded the result.

- Skip getCurrentRowStates on replication: prepareReplicatedIndexMutations reads
  only the group's PRE_IMAGE/nextState, never dataRowStates, so the scan and its
  pendingRows/lastConcurrentBatchContext bookkeeping are inert on the standby.
- Dispatch to a new preBatchMutateReplication right after currentPhase = PRE and
  return, so the active body runs only on the active. Drop the five now-dead
  isReplication guards there.
- Extract the shared two-phase commit (preparePreIndexMutations -> unlock ->
  doPre -> lock -> [wait] -> post) into prepareAndCommitGlobalIndexUpdates so the
  lockstep protocol stays single-sourced; the active/standby fork lives inside
  preparePreIndexMutations. The concurrent-batch wait is now reached only when
  lastConcurrentBatchContext is set, which never happens on the standby.

Verified: IndexRegionObserverReplayTest 16/16, ReplicationLogGroupIT 15/15
(incl. testConcurrentUpserts, concurrent same-row standby replay).
… cell filter

Both replication paths now drop local-index (L#) cells the same way. The
WAL-restore path (replicateEditOnWALRestore) previously shipped the persisted
WAL edit verbatim, leaking the L# cells HBase merged into the data mutation's
family map -- a recovery-path-only leak no IT exercised. It now filters the
cell stream through the shared isReplicableCell predicate.

The synchronous path (replicateMutations) reads the batch's now-final cells
from miniBatchOp in POST and drops L# cells with a single family-key check per
family list, then appends the pre-image cells from WAL slot 0 (filtered through
isReplicableCell). capturePreImageCells is now the sole producer of pre-image
cells, written only to the WAL edit and read back by both paths, so they ship
byte-identical output with no re-derivation.

Removes the timing-based snapshot machinery (captureReplicationCells and the
replicationCellsByRow field) that the sync path used to exclude L# cells, since
the WAL-restore path could not use it.
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