[RLC] Case 3 — exclude relocation (dataChange=false) files from the append-conflict check - #10
Closed
sezruby wants to merge 3 commits into
Closed
[RLC] Case 3 — exclude relocation (dataChange=false) files from the append-conflict check#10sezruby wants to merge 3 commits into
sezruby wants to merge 3 commits into
Conversation
… check OPTIMIZE (compaction / Z-ORDER) commits its compacted outputs as dataChange=false AddFiles. Because OPTIMIZE is not a blind append (isBlindAppend=false), those files land in changedDataAddedFiles, so a concurrent non-blind writer (UPDATE/DELETE/MERGE with a read predicate) hits a spurious ConcurrentAppendException -- even though OPTIMIZE changed no logical data. A dataChange=false file only rearranges rows that already existed, so it can never be a row the losing txn "should have read." Add conflictDetection.excludeNoDataChangeAddedFiles.enabled (internal, default off) that filters dataChange=false files out of the added-files check in checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn. All FileActions in a commit share one dataChange value (see trackConsistentDataChange), so this is effectively a commit-level gate. Tests: OptimisticTransactionSuite +5 cases -- OPTIMIZE-style dataChange=false winner vs (a) a concurrent partition read and (b) a concurrent whole-table read (the unpartitioned / Liquid Clustering scenario), each flag on -> reconcile / off -> legacy abort; plus a safety-floor case proving a genuine dataChange=true append still raises ConcurrentAppendException with the flag on. Full OptimisticTransactionSuite 148/148 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validate the exclusion against a *realistic* OPTIMIZE that both removes the compacted inputs and adds the dataChange=false output (the earlier tests only modeled the added file): - full OPTIMIZE vs an insert-only reader that registered only a partition read predicate (filterFiles(newFiles) does not populate readFiles) -> the separate removed-files check cannot fire, so with the flag on the txn FULLY reconciles (not merely swapping ConcurrentAppendException for ConcurrentDeleteRead). This is the exact insert-only shape reported in delta-io#326 / delta-io#626 / delta-io#1305. - full OPTIMIZE vs a whole-table reader (readFiles populated) -> still raises ConcurrentDeleteReadException with the flag on, proving the added-files fix does not (and must not) silence a genuine read/remove overlap. Fully reconciling that case is the harder row-level-concurrency problem, out of scope here. Dropped the earlier unpartitioned 'whole-table read reconciles' test: its winner added a dataChange=false file without removing anything, which overstated the fix (a real OPTIMIZE removes, and a whole-table reader then hits the delete-read check as shown above). Full OptimisticTransactionSuite 148/148 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nly) Broaden the exclusion so an append-only writer no longer raises a spurious ConcurrentDeleteReadException when a concurrent OPTIMIZE (dataChange = false) merely relocated the files it read. A dataChange = false RemoveFile deletes no logical rows -- every row survives under a new file boundary -- so it cannot invalidate a read done by a transaction that itself adds no RemoveFile and no deletion vector. The exclusion is gated on the current transaction being append-only: a DML loser (delete/update) reads files precisely to rewrite them, so a concurrent relocation of a read file must still conflict, else its RemoveFile/DV would target a file that no longer exists. Genuine deletes commit dataChange = true and are always kept, so real delete/read conflicts still fire. Rename the flag DELTA_CONFLICT_DETECTION_EXCLUDE_NO_DATA_CHANGE_ADDS -> DELTA_CONFLICT_DETECTION_EXCLUDE_NO_DATA_CHANGE_FILES (conflictDetection.excludeNoDataChangeFiles.enabled) to reflect that it now covers both added and removed files; still default-off. Tests (OptimisticTransactionSuite, 150/150): - whole-table APPEND-ONLY reader vs full OPTIMIZE -> reconciles (was: raised) - DML (delete) loser vs OPTIMIZE relocation -> still ConcurrentDeleteRead - genuine dataChange=true delete vs append-only reader -> still conflicts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
Superseded — opened publicly as delta-io#7331 (branch unchanged). Closing this fork-internal review pass. |
This was referenced Aug 3, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the row-level-concurrency umbrella (#3) — Case 3. Full design: #6. Opened upstream as delta-io#7331 (this fork PR is superseded by that one).
Problem
A concurrent transaction that only rearranges existing rows — most importantly
OPTIMIZE(compaction / Z-ORDER) — commits its outputs asAddFiles withdataChange = falseand removes its compacted inputs asRemoveFiles, also withdataChange = false. BecauseOPTIMIZEis not a blind append (isBlindAppend = false), theWinningCommitSummaryclassifies those adds aschangedDataAddedFiles.As a result an insert-only / append-only writer running concurrently fails spuriously:
ConflictChecker.checkForAddedFilesThatShouldHaveBeenReadByCurrentTxntreats theOPTIMIZEoutput as newly-arrived data →ConcurrentAppendExceptionif the writer's read predicate overlaps.readFiles(e.g. a whole-table read),checkForDeletedFilesAgainstCurrentTxnReadFilesseesOPTIMIZEremoved a file it read →ConcurrentDeleteReadException.Both fire even though
OPTIMIZEintroduced no new logical rows and deleted none — it only relocated existing rows across file boundaries. This is the exact symptom in delta-io#626 ("Prevent insert-only transactions from failing due to concurrent data preserving transactions"), the stale delta-io#1305, and the closed issue delta-io#326. Both prior PRs stalled on the reviewer's request (delta-io#1305) for a general safety argument rather than a targeted patch. This PR supplies that argument — now materially stronger than it could have been in 2022 — and closes both failure modes for an append-only writer.The safety argument (now backed by a validated invariant)
A file committed with
dataChange = falsecarries, by contract, only rows that already existed in the table at that version. It contributes zero new logical rows and deletes zero logical rows — it rearranges/compacts existing rows into new file boundaries.Added-files check. Its purpose is to catch rows a loser should have read but didn't because a concurrent writer added them. A
dataChange = falseadd can never hold such a row: every logical row in it was already present (in some other file) in the loser's read snapshot. So it is safe to drop from that check.Removed-files-vs-read check (new in this PR). Its purpose is to catch a loser that read a file whose rows were concurrently deleted. A
dataChange = falseremove deleted no rows — the rows it dropped from that file still exist under a new file boundary. So an append-only writer that read the relocated file read nothing that was logically removed, and can safely reconcile. This exclusion is gated on the current transaction being append-only (it adds noRemoveFileand noAddFilecarrying a deletion vector): a DML loser (DELETE/UPDATE/MERGE) reads a file precisely to rewrite it, so if that file was concurrently relocated it must still conflict — otherwise its ownRemoveFile/DV would target a file that no longer exists and rows could be lost or resurrected.The 2022 concern was essentially "could a commit mix data-changing and data-preserving files such that filtering by
dataChangeskips something that matters?" That mixed case can no longer exist: delta-io#6937 / delta-io#6969 ("Validate consistent dataChange across commit FileActions", merged upstream) enforce as a validated invariant that allFileActions in a single commit share onedataChangevalue (ConflictChecker.trackConsistentDataChange, fatal mode throws on violation). Therefore both.filter(_.dataChange)sites are provably whole-commit gates: either the entire winning commit was data-preserving (safe) or it changed data (checked exactly as before).The change cannot mask a genuine conflict: a winner that adds real rows commits them
dataChange = true(still fully checked), and a winner that genuinely deletes rows commits itsRemoveFiledataChange = true(never excluded) — both covered by safety-floor tests.(Databricks' row-level-concurrency contract documents that non-Z-Order
OPTIMIZE"cannot conflict" with concurrentINSERT/UPDATE/DELETE/MERGE. This PR was implemented independently against that observable contract and the OSSConflictCheckersemantics.)Scope (validated by tests)
Behind a single internal flag (default off), for an append-only current transaction:
dataChange = falseadds are dropped → no spuriousConcurrentAppendException.dataChange = falseremoves are dropped → no spuriousConcurrentDeleteReadException, for both a predicate-only reader and a whole-table (readFiles-populated) reader.Deliberately not touched:
RemoveFile/DV) overlapping anOPTIMIZEremove → stillConcurrentDeleteReadException. Fully reconciling that is the harder row-level-concurrency problem (Case 4, [RLC] Case 4 — OPTIMIZE ⟂ DML reconciliation (compaction offset remap) #7).dataChange = trueremove) → still conflicts with a reader of that file.Change
Internal flag
spark.databricks.delta.conflictDetection.excludeNoDataChangeFiles.enabled(default off). Added-files site filtersaddedFilesToCheckForConflicts.filter(_.dataChange); removed-files-vs-read site filterswinningCommitSummary.removedFiles.filter(_.dataChange), gated on the current txn being append-only (forallover its actions: noRemoveFile, noAddFilewith a deletion vector).Tests
OptimisticTransactionSuite:dataChange = false append (OPTIMIZE-style) vs concurrent partition read— flag on → reconcile, off → legacyConcurrentAppendException.dataChange = true append still conflicts …(safety floor) — a genuine new-data append still conflicts with the flag on.full OPTIMIZE (removes + dataChange=false adds) vs insert-only partition reader reconciles— predicate-only reader, Prevent insert-only transactions from failing due to concurrent data preserving transactions delta-io/delta#626 shape.full OPTIMIZE (removes + dataChange=false adds) vs whole-table APPEND-ONLY reader reconciles— whole-tablereadFilesreader now reconciles too (the removed-files half of the fix).full OPTIMIZE (dataChange=false removes) vs concurrent DELETE loser still raises ConcurrentDeleteReadException— boundary: the append-only guard holds; a DML loser is not silenced.genuine delete (dataChange=true remove) vs append-only reader still raises ConcurrentDeleteReadException— safety floor for removes: real deletions are never excluded.Prior art
dataChangevalidation this argument builds on.