Skip to content

[RLC] Case 4a — OPTIMIZE loses to DML (forward compaction offset remap) - #9

Open
sezruby wants to merge 3 commits into
optimize-dv-remap-capturefrom
optimize-dv-remap
Open

[RLC] Case 4a — OPTIMIZE loses to DML (forward compaction offset remap)#9
sezruby wants to merge 3 commits into
optimize-dv-remap-capturefrom
optimize-dv-remap

Conversation

@sezruby

@sezruby sezruby commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Part of the row-level-concurrency umbrella (#3) — Case 4a (forward direction: a compaction OPTIMIZE that loses to a concurrent DML). Full design: #7. The shared capture/persist write side is PR #12 (this stacks on it); the reverse DML-loser direction is PR #11 (stacked on this).

What

Opt-in reconciliation (spark.databricks.delta.optimize.conflictReconciliation.enabled, internal, default off) for a compaction OPTIMIZE that loses to a concurrent row-level DELETE/UPDATE. Instead of aborting the loser, ConflictChecker.resolveOptimizeConflicts reads the source composition PR #12 persisted, remaps the concurrent deletion vector from each removed source onto the compacted output, unions it into that output's DV, and re-points the source RemoveFile at the winner's post-image (files keyed by path and DV) — so both commit.

Mechanism

Compaction concatenates each source file's live rows into one contiguous run in the output, so the reconcile reduces to where each source row landed. From the persisted compactedInto / compactionInfo tags, ConflictChecker.parseOptimizeSourceComposition recovers (outputPath, outputStart, liveCount) per removed source. For each of the winner's newly-deleted physical rows it computes the output position, unions it into the output DV, and commits both. All-resolvable-or-abort — any row that can't be placed falls back to today's abort.

Offset remap with lazy read-time-gap reconstruction

A source that was fully live at read time maps physical row ioutputStart + i. A source that carried a read-time DV has physically non-contiguous live rows, but the tag records only its physical count — the gaps are reconstructed lazily at conflict time:

liveRank(i) = i − (# read-time deletions below i)     // binary search on the sorted read-time DV
outputPos   = outputStart + liveRank(i)

Only the winner's incremental deletions (winnerDv \ readTimeDv) are remapped; positions already deleted at read time were never written to the output and are skipped. A row whose liveRank lands outside the source's recorded run → abort.

Scope / guards

Reconciles only what PR #12 captured (order-preserving coalesce compaction). Reclustering / ZORDER and the repartition path record no composition → this falls back to today's abort, which matches the documented RLC contract (a non-ZORDER OPTIMIZE "cannot conflict" — here it simply doesn't, by reconciling). Safe because opt-in: losing the reconcile restores current behavior, never wrong data.

Dependency

Stacked on PR #12 (Case 4 capture/persist, base branch optimize-dv-remap-capture), itself stacked on the same-file DV-merge PoC (Case 2 — #5 / PR #2). Reuses that PoC's DV helpers and rowLevelResolvedPaths.

Tests — OptimizeConflictReconciliationSuite (forward + parser)

Reconcile behavior:

  • reconciles a concurrent DELETE / UPDATE by remapping the DV onto the compacted output;
  • reconciles a DELETE when the source already carried a read-time DV (lazy gap reconstruction), incl. multiple gaps in one source;
  • remaps a delete to a non-DV file in a bin that also holds a DV'd file;
  • targets the correct output among multiple compaction bins;
  • partitioned compaction reconciles, and the capture reports the partition ordering (no sort above the capture);
  • reclustering (ZORDER) and repartition-path losers are not reconciled and abort.

Parser (composition tag:): single-run parse, live = physical − read-time-DV, tolerates unknown fields (schema drift), falls back to None on shapes the contiguous offset remap can't model.

🤖 Generated with Claude Code

@sezruby
sezruby force-pushed the optimize-dv-remap branch from 08bb237 to a7dccb1 Compare July 27, 2026 21:35
@sezruby
sezruby force-pushed the optimize-dv-remap branch 2 times, most recently from 29063e2 to 996f9bd Compare August 3, 2026 00:54
@sezruby sezruby changed the title Optimize: reconcile compaction OPTIMIZE vs concurrent DML (offset DV remap) [RLC] Case 4a — OPTIMIZE loses to DML (forward compaction offset remap) Aug 3, 2026
@sezruby
sezruby force-pushed the optimize-dv-remap branch 2 times, most recently from bf533fc to b0412f6 Compare August 4, 2026 06:43
@sezruby
sezruby changed the base branch from row-level-concurrency-dv-poc to optimize-dv-remap-capture August 4, 2026 06:48
@sezruby
sezruby force-pushed the optimize-dv-remap-capture branch from f27d569 to 7cda6c7 Compare August 4, 2026 19:01
@sezruby
sezruby force-pushed the optimize-dv-remap branch from b0412f6 to 1009bfc Compare August 4, 2026 19:01
When a compaction OPTIMIZE loses a commit race to a concurrent row-level
DML (DELETE/UPDATE), remap the winner's deletion vector onto the OPTIMIZE
output using the persisted source composition, instead of aborting the
OPTIMIZE.

  - ConflictChecker.resolveOptimizeConflicts reads each removed source's
    compactedInto / compactionInfo tombstone tag (via
    parseOptimizeSourceComposition) to recover (outputPath, outputStart,
    liveCount), then rebases the winning DML's new deletions onto the
    compacted output by contiguous offset + live rank. O(1) per source.
  - Conservative and safe-by-abort: only a pure compaction, only single-run
    contiguous placements, output bound checked; any absent or malformed
    tag or unmodeled shape falls back to today's abort, never a wrong
    result.
  - Gated by optimize.conflictReconciliation.enabled (default off).

Builds on the capture/persist layer (compactedInto / compactionInfo tags,
CompactionInfoEntry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sezruby and others added 2 commits August 7, 2026 22:39
resolveOptimizeConflicts reads, remaps and rewrites deletion vectors as
driver-side object-store I/O during conflict detection. A failure there
(unreadable/corrupt DV, transient I/O) would surface an unexpected error
out of the conflict checker instead of the retryable Concurrent*
exception callers expect.

Reconciliation is a pure optimization over the conservative abort: the
transaction is mutated only on the success path. Wrap the work in
try/catch(NonFatal) so any DV read/merge/write failure leaves the
transaction untouched and the standard file-level checks abort cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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