Provide a REPAIRED preview that audits already-reconciled data across replicas for a tracked keyspace, validating that data backing reconciled offsets is byte-identical across replicas. Reject the operation if the keyspace isn't fully migrated to mutation tracking or if topology changes during any phase. ALL and UNREPAIRED previews on tracked keyspaces are a separate (simpler) design — out of scope here.
Identical to classic REPAIRED preview:
nodetool repair --validate <keyspace>
--validate maps to PreviewKind.REPAIRED (tools/nodetool/Repair.java:96-97, 137-139). The operator does not learn a second command. Routing between classic PreviewRepairTask and the tracked-keyspace path is invisible to them.
The audit needs a cut: a per-log offset frontier C such that every live participant is known to have reconciled at least up to C. Below C, replicas must agree by contract; any observed disagreement is a real bug (MT bookkeeping, journal corruption, SSTable corruption, or operator intervention).
Establishing such a cut is exactly what a real MT incremental repair does as a side effect: MutationTrackingIncrementalRepairTask completes only when every live participant's reconciled offsets contain the target captured at IR start (see MutationTrackingSyncCoordinator.ShardSyncState.isComplete — MutationTrackingSyncCoordinator.java:385-402). We abstract this behind a CutEstablisher interface and reuse the IR machinery as the default implementation, but the audit depends on the cut, not on IR per se.
MT IR does not touch SSTable metadata. SSTableWriter.finalizeMetadata:362-378 sets repairedAt only at flush time based on MutationTrackingService.isDurablyReconciled(coordinatorLogOffsets). Compaction can re-check via CompactionTask.getCoordinatorLogOffsets:446-453, but MT IR skips anti-compaction and does no streaming, so it does not indirectly trigger this either. What IR does do — and this is what makes it useful for the audit — is advance every live replica's reconciled watermark to include every offset that any live replica had witnessed at IR start. That is: IR raises the cut, without touching a single SSTable byte.
Every SSTable's coordinatorLogOffsets (SSTableWriter.java:88, 117, 385; read via SSTableReader.getCoordinatorLogOffsets() at line 1233) records exactly which (log, offset) pairs were applied to its source memtable. For an SSTable S that existed on any replica at IR start, every offset in S.coordinatorLogOffsets was witnessed by that replica (that's how it got into the memtable), so it's in T = union(witnessed). IR waits until every live replica reconciles ⊇ T, so post-IR C = min(reconciled) ⊇ T. Therefore every offset in S.coordinatorLogOffsets is ≤ C. S is fully below cut — not straddling.
The only SSTables that can straddle are those flushed after IR start (memtables that absorbed post-IR-start writes). These are addressed by snapshotting the SSTable set at IR start on each participant, so any post-IR-start flush produces an SSTable that is not in the audit's set at all.
-
CutEstablisher— interface (inorg.apache.cassandra.replication). Contract: given a range set and participant set, produce anAuditCutsuch that for every live participantp,p.reconciled ⊇ cut, and ensure each participant has captured a stable snapshot of its SSTable set + journal state before the returned future completes. Fail loud on topology change (never return a stale cut) and on participant timeout / unreachable.interface CutEstablisher { Future<AuditCut> establishCut(RepairJobDesc desc, NeighborsAndRanges neighborsAndRanges, Epoch expectedEpoch); }A future implementation could establish the cut without running full IR (e.g. by intersecting reconciled offsets sampled at a single point in time). One implementation for now.
-
IncrementalRepairCutEstablisher— the default (and currently only)CutEstablisherimplementation, alongsideMutationTrackingIncrementalRepairTaskinorg.apache.cassandra.repair. Orchestrates a two-phase preparation on each participant:- Sends
MT_PREVIEW_PREPARE_REQto every participant. Each participant captures its current SSTable set (viaSSTableReader.Refsso compaction can't reclaim the files during audit) and pins aMutationJournal.Snapshot, caching both againstparentSession(seeAuditPreparationCachebelow). Responds when the snapshot is stable. - After all participants ack, runs an internal
MutationTrackingIncrementalRepairTaskfor the same range/participant set. On completion, extracts targets from the sync coordinators'ShardSyncState.targets(via the accessor we need to add — currently private atMutationTrackingSyncCoordinator.java:371) and wraps them in anAuditCut. - Completes the returned future with the
AuditCut.
On failure, completes with a
CutEstablishmentExceptionwrapping the underlying cause. Failure also triggers snapshot release on every participant that had prepared. - Sends
-
AuditCut— immutable value object wrappingImmutableMap<Range<Token>, ImmutableMap<CoordinatorLogId, Offsets.Immutable>>(shard range → per-log offset frontier). Exposes:containsAll(Range<Token> shardRange, ImmutableCoordinatorLogOffsets sstableOffsets) → boolean— used to test whether an SSTable is fully ≤ cut for its overlapping shard.containsMutation(ShortMutationId id) → boolean— used to filter journal-replay mutations. Same shape asReconciledLogSnapshot.isFullyReconciled(keyspace, ShortMutationId)(ReconciledLogSnapshot.java:49-55), just sourced from the IR-captured cut rather than the local reconciled state.
-
AuditPreparationCache— participant-side state, one entry perparentSession, holding(Refs<SSTableReader> sstables, MutationJournal.Snapshot journal, Epoch epoch). Populated onMT_PREVIEW_PREPARE_REQ, read onMT_AUDIT_VALIDATE_REQ, released onMT_AUDIT_VALIDATE_RSP, onMT_PREVIEW_RELEASE_REQ, or on timeout. Bounded by session lifetime; each entry has an idle-timeout (say2 × mutation_tracking_sync_timeout) so a crashed coordinator doesn't leak refs forever. -
AuditValidationSource— implements the existingValidationPartitionIteratorcontract thatValidatorconsumes (repair/Validator.java). Consumes anAuditPreparationCacheentry (the snapshotted SSTables + pinned journal) and anAuditCut. Yields rows from SSTables passingcut.containsAll(...)interleaved with journal-replay rows passingcut.containsMutation(...), all filtered to the requested token range. -
MutationTrackingRepairedPreviewTask extends AbstractRepairTask— top-level orchestrator for the audit. Sibling ofMutationTrackingIncrementalRepairTask. UsesCutEstablisherfor phase 1; does not referenceMutationTrackingIncrementalRepairTaskdirectly. -
MutationTrackingPreviewPrepareRequest/Response—RepairMessagepayloads for the new verb pairMT_PREVIEW_PREPARE_REQ/MT_PREVIEW_PREPARE_RSP. Sent byIncrementalRepairCutEstablisherto each participant before triggering IR. Request carries(parentSession, range, table, Epoch expectedEpoch). Response is an ack once the snapshot is in place. -
MutationTrackingAuditRequest/Response—RepairMessagepayloads forMT_AUDIT_VALIDATE_REQ/MT_AUDIT_VALIDATE_RSP. Request carries(parentSession, range, table, AuditCut, Epoch expectedEpoch). Participant looks up its prepared snapshot byparentSession. Response is an ack; the merkle tree itself ships over the existingValidator → ValidationCompletepath. -
MutationTrackingPreviewReleaseRequest— one-way payload forMT_PREVIEW_RELEASE_REQ. Coordinator sends to every participant in step 7 to explicitly release cached snapshots.
At the start of MutationTrackingRepairedPreviewTask.performUnsafe:
- Reject if
!ksm.useMutationTracking()ormetadata.mutationTrackingMigrationState.isMigrating(keyspace)(existing helpersMutationTrackingIncrementalRepairTask.shouldUseMutationTrackingRepairatMutationTrackingIncrementalRepairTask.java:195-212andisMutationTrackingMigrationInProgressat lines 223-226 already encode these tests). - Capture
Epoch epochAtStart = ClusterMetadata.current().epochfor topology checks in steps 3 and 4.
Invoke the default CutEstablisher (currently IncrementalRepairCutEstablisher):
CutEstablisher establisher = MutationTrackingService.instance().cutEstablisher();
Future<AuditCut> cutFuture = establisher.establishCut(desc, neighborsAndRanges, epochAtStart);
Emit "establishing cut" progress notification before the call.
Under the hood, IncrementalRepairCutEstablisher does:
- Prepare: send
MT_PREVIEW_PREPARE_REQto every participant. Each capturesRefs<SSTableReader>for the target table + aMutationJournal.Snapshot, caches byparentSessioninAuditPreparationCache, and responds. This freezes the auditable state on each participant. - Run IR: invoke an internal
MutationTrackingIncrementalRepairTaskfor the same range/participants. Waits for reconciled ⊇T = union(witnessed)on every live participant. - Assemble: extract targets from
ShardSyncState.targets, buildAuditCut, complete the returned future.
Emit "cut established" on success.
On failure at any sub-step: complete the future with CutEstablishmentException wrapping the underlying cause; also send MT_PREVIEW_RELEASE_REQ to any participants that had already prepared. Surface to the operator as "REPAIRED preview failed: unable to establish audit cut (<cause>)".
Before dispatching audit-validate RPCs, assert ClusterMetadata.current().epoch.equals(epochAtStart). Fail with a clear message if not, and send MT_PREVIEW_RELEASE_REQ to every participant to free the cached snapshots.
Emit "dispatching audit" progress notification. Send MutationTrackingAuditRequest(parentSession, range, table, AuditCut, epochAtStart) over MT_AUDIT_VALIDATE_REQ. Each participant, on receipt:
- Verifies
ClusterMetadata.current().epoch.equals(expectedEpoch); rejects withTopologyChangedif not. - Looks up its
AuditPreparationCacheentry forparentSession. If missing (e.g., participant restarted between prepare and validate), rejects withPreparationMissing. - Constructs an
AuditValidationSourceover(cache.sstables, cache.journal, AuditCut), feeds it to aValidator, and periodically re-checks the TCM epoch — aborting on any change. - Returns the merkle tree via the existing
ValidationCompletepath.
Tree input is the union of:
- SSTables: iterate the snapshotted
Refs<SSTableReader>set (captured at IR start). IncludesstableiffauditCut.containsAll(sstable.range, sstable.getCoordinatorLogOffsets())— strict containment, fully below cut. Because the SSTable set was frozen at IR start and IR raises the cut past every offset witnessed at IR start, every snapshotted SSTable's offsets are ≤ cut. Straddling within the snapshotted set is therefore not possible under normal operation; any straddling SSTables would represent bugs (e.g. a snapshotted SSTable somehow contains offsets that weren't witnessed by any live replica at IR start), so the strict check remains as a defensive guard rather than an expected filter path. - Journal: replay the pinned
MutationJournal.Snapshot, including mutationmiffauditCut.containsMutation(m.id()).
All output filtered to the requested token range.
Any post-IR-start flush is invisible to the audit because the resulting SSTable is not in Refs<SSTableReader> — it's a genuinely new SSTable that appeared after the snapshot. Same for post-IR-start writes that stay in memtables (never in Refs) and post-IR-start journal segments (the pinned MutationJournal.Snapshot covers only segments that existed at pin time).
Tree exchange and pairwise comparison are unchanged from existing repair. Build a SyncStatSummary from the diffs (same shape as PreviewRepairTask.java:75-93). On non-empty summary, increment RepairMetrics.previewFailures and, for any range mismatch, reuse PreviewRepairTask.maybeSnapshotReplicas (PreviewRepairTask.java:114-168) to take diagnostic snapshots on every participant when snapshot_on_repaired_data_mismatch is enabled. Emit "audit complete" progress notification. Emit the reused "Repaired data is in sync" / "Repaired data is inconsistent\n<summary>" message so operators see one output shape across classic and tracked previews.
Metric semantics note: bytesPreviewedDesynchronized and tokenRangesPreviewedDesynchronized are populated using the same SyncStatSummary accounting as classic REPAIRED preview so operators can dashboard uniformly. For tracked keyspaces, "bytes" here counts the merged SSTable + journal-replay stream rather than pure on-disk bytes; the number is close enough to be operationally useful but is not directly comparable to raw disk bytes. Documented at the task's javadoc and in operator-facing release notes.
On success or failure, the coordinator sends MT_PREVIEW_RELEASE_REQ to every participant so it can:
- Release
Refs<SSTableReader>(allowing compaction to reclaim the pinned SSTable files). - Close its pinned
MutationJournal.Snapshot(MutationJournal.java:119). - Remove the entry from
AuditPreparationCache.
Participants also apply an idle-timeout to cache entries (say 2 × mutation_tracking_sync_timeout) so a crashed coordinator doesn't leak refs forever. The coordinator emits a final notification with the result.
RepairCoordinator.repair (RepairCoordinator.java:557-...) currently routes preview before checking MT:
if (state.options.isPreview()) → PreviewRepairTask
else if (useMutationTracking) → MutationTrackingIncrementalRepairTask
Becomes:
if (state.options.isPreview() && useMutationTracking) {
if (previewKind == REPAIRED && !migrating) → MutationTrackingRepairedPreviewTask
else → reject with clear error
}
else if (state.options.isPreview()) → PreviewRepairTask // existing
else if (useMutationTracking) → MutationTrackingIncrementalRepairTask
MutationTrackingRepairedPreviewTask emits the following via coordinator.notifyProgress(...) at internal milestones. These are the routing marker and latch-coordination surface for tests and the observable pipeline shape for operators inspecting notification streams:
"establishing cut"— before invokingCutEstablisher.establishCut(...). Earliest MT-specific marker."cut established"— when the cut future resolves successfully. By this point, every participant's SSTable + journal snapshot has already been captured (that happened during theMT_PREVIEW_PREPARE_REQsub-step, before IR ran)."dispatching audit"— before sendingMT_AUDIT_VALIDATE_REQ."audit complete"— after collecting all trees and computing the summary. Immediately followed by the reused"Repaired data is in sync"/"Repaired data is inconsistent\n<summary>"message.
- Migration handling. Operator must wait for full migration before running REPAIRED preview.
- Partial results on topology change. Hard failure with retry guidance — no "audit N of M participants" mode.
- Cheap previews. This is at-least cut-establishment cost, which today is at-least IR cost. The
UNREPAIREDMT preview (offset arithmetic only, no cut-establishment IR, no streaming) covers the cheap case separately. - Force flush before audit. Considered as a way to eliminate straddling SSTables; not needed once the SSTable set is snapshotted at IR start and IR raises the cut past all pre-existing witnessed offsets.
- Row-level mutation-ID tagging on SSTables. Would be needed if we were relying on filtering rows out of straddling SSTables. Not needed because snapshotted SSTables can't straddle under the IR-first + snapshot-at-IR-start design. Left as future work if a scenario ever demands it.
- Operator-visible
--skip-establish-cutor--audit-onlyflags. The cut is a required precondition for a meaningful audit, and there is no operator-facing way to bypass it. A future lighter-weightCutEstablisherimplementation would preserve the same operator interface.
Validator and its merkle-tree machinery, ValidationComplete message path, range-difference computation, SyncStatSummary reporting, RepairMetrics.previewFailures, PreviewRepairTask.maybeSnapshotReplicas, and the existing MutationJournal.Snapshot / Refs<SSTableReader> / ImmutableCoordinatorLogOffsets / ShortMutationId types. The task does not directly know about MutationTrackingIncrementalRepairTask — that dependency is confined to IncrementalRepairCutEstablisher.
CutEstablisherinterface (small).IncrementalRepairCutEstablisher(orchestrates prepare → IR → cut assembly).AuditCut(small immutable value object).AuditPreparationCache(participant-side, session-keyed).AuditValidationSource(interleaves snapshotted SSTable and journal streams intoValidationPartitionIteratorshape).MutationTrackingRepairedPreviewTask(orchestration).- Two new verb pairs:
MT_PREVIEW_PREPARE_REQ/RSPandMT_AUDIT_VALIDATE_REQ/RSP, plus the one-wayMT_PREVIEW_RELEASE_REQ. - Corresponding
RepairMessagepayload classes. - Accessor on
MutationTrackingSyncCoordinator.ShardSyncStateto expose captured targets toIncrementalRepairCutEstablisher. CutEstablishmentException(thin wrapper for failure attribution).