fix(compaction): preserve row order across parallel tasks - #8400
fix(compaction): preserve row order across parallel tasks#8400lance-gatefixer[bot] wants to merge 16 commits into
Conversation
|
Important This PR touches the Lance format specification. Substantive changes to the format specification — the If this is a meaningful format change:
|
|
Addressed in 93b75ef. The repair now retains the ID-sorted manifest contract by reserving one ordered replacement suffix and metadata-only relabeling untouched trailing fragments. The logical-order feature bit, protobuf/spec changes, and format-vote dependency are removed; focused JsonIndex/FtsIndex upgrade-downgrade tests pass against the locally available historical releases. |
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
The suffix cap bounds relabel work, but it replaces the established oldest-first incremental behavior with tail-only planning that can permanently leave eligible fragments untouched.
A viable revision must preserve a progress contract as well as row order and bounded work. Decouple logical order from fresh fragment IDs, or make tail-only/no-progress semantics an explicit maintainer decision instead of describing this option as bounded incremental compaction.
| candidate_bins = candidate_bins | ||
| .into_iter() | ||
| .filter_map(|mut bin| { | ||
| if bin.pos_range.end <= suffix_start { |
There was a problem hiding this comment.
This hard suffix cut can permanently strand eligible compaction work. With fragment row counts [100, 100, 1000, 1000], target_rows_per_fragment=250, and max_source_fragments=2, fragments 0 and 1 are an eligible pair, but the healthy final pair makes suffix_start=2, so this branch removes the only candidate bin. Every repeated bounded run therefore returns an empty plan rather than advancing the oldest eligible work.
The planner needs to keep earlier eligible work reachable within the accepted contract. If that requires logical order independent of fresh IDs, the format decision must be explicit rather than silently turning this incremental option into a permanent no-op.
Reproducer
I added this focused test on the current head:
#[tokio::test]
async fn test_max_source_fragments_progresses_before_healthy_suffix() {
let test_dir = TempStrDir::default();
let data = sample_data();
let schema = data.schema();
let fragment_rows = [100, 100, 1_000, 1_000];
let write_params = WriteParams {
max_rows_per_file: 1_000,
..Default::default()
};
Dataset::write(
RecordBatchIterator::new(vec![Ok(data.slice(0, fragment_rows[0]))], schema.clone()),
&test_dir,
Some(write_params.clone()),
)
.await
.unwrap();
let mut offset = fragment_rows[0];
for row_count in fragment_rows.iter().copied().skip(1) {
let mut append_params = write_params.clone();
append_params.mode = WriteMode::Append;
Dataset::write(
RecordBatchIterator::new(vec![Ok(data.slice(offset, row_count))], schema.clone()),
&test_dir,
Some(append_params),
)
.await
.unwrap();
offset += row_count;
}
let dataset = Dataset::open(&test_dir).await.unwrap();
let options = CompactionOptions {
target_rows_per_fragment: 250,
max_source_fragments: Some(2),
..Default::default()
};
let plan = plan_compaction(&dataset, &options).await.unwrap();
let planned_fragment_ids = plan
.tasks()
.iter()
.flat_map(|task| task.fragments.iter().map(|fragment| fragment.id))
.collect::<Vec<_>>();
assert_eq!(planned_fragment_ids, vec![0, 1]);
}cargo test -p lance test_max_source_fragments_progresses_before_healthy_suffix -- --nocaptureExpected [0, 1]; observed [].
There was a problem hiding this comment.
No code change was made at 56ee824 because the requested combination is incompatible with the current stable table contract. Rewrite transactions must use freshly reserved fragment IDs, and released readers require manifests to remain ID-sorted. An early rewrite must therefore either relabel every trailing fragment (exceeding max_source_fragments) or change row order. Restoring oldest-first bounded progress requires the separately reviewed logical-order format feature and its maintainer/PMC decision; source-ID reuse would violate the fragment-ID high-water invariant.
There was a problem hiding this comment.
No code change was made at d23b189. The requested combination remains incompatible with the released stable contract: fresh fragment IDs plus ID-sorted manifests require either relabeling the full trailing suffix (exceeding the strict bound) or changing row order. Oldest-first bounded progress therefore requires a maintainer choice to relax the bound or approve a logical-order format feature.
|
No code change was made at 56ee824. The requested |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
# Conflicts: # rust/lance/src/dataset/optimize.rs
|
No code change was made at d23b189. The current implementation takes the strict affected-fragment-bound option while preserving ID-sorted stable manifests and row order; guaranteeing oldest-first bounded progress as well would require either relaxing that bound or approving the separately reviewed logical-order format feature. Selecting that contract is a maintainer/PMC decision, not an implementation choice this repair can assume. |
# Conflicts: # java/src/main/java/org/lance/compaction/CompactionOptions.java # python/python/lance/dataset.py # rust/lance/src/dataset/optimize.rs
|
Blocked: PR #8400 requires a maintainer decision on the At remote head The repair already implements and validates the strict-bound option while preserving row order and ID-sorted released manifests; all current actionable inline findings have verified App dispositions, and the focused compaction suite passes 128 tests. No code-only change can select among these policy contracts without maintainer authority. The smallest unblock is for a maintainer to confirm the strict-bound semantics. Alternatively, explicitly permit suffix work beyond the bound, or approve the logical-order format path and its required process. |
…er/fix-3465-1 # Conflicts: # rust/lance-table/src/transaction.rs
…er/fix-3465-1 # Conflicts: # rust/lance/src/dataset/optimize.rs
There was a problem hiding this comment.
🟡 Gate recommendation: maintainer decision required.
The current revision preserves row order, the released ID-sorted manifest contract, and excluded fragment identities by limiting rewrites to one relabeled suffix. The verified consequence is that max_source_fragments can permanently strand older eligible work; after the latest merge, a caller or unreadable-index exclusion likewise makes every earlier candidate indefinitely ineligible.
Maintainers need to choose whether those options may sacrifice compaction progress, whether the bound and exclusion-identity guarantees may be relaxed, or whether logical fragment order should become a format feature. If strict affected-fragment bounds, immutable exclusions, and oldest-first progress are all required, the logical-order format path is necessary, with its compatibility boundary and PMC approval.
|
Blocked: PR #8400 requires a maintainer decision on the At remote head The repair branch contains the current The smallest unblock is for a maintainer to confirm that strict bound and exclusion semantics may sacrifice compaction progress. Alternatively, explicitly permit suffix relabel/remap work beyond the bound, or approve the logical-order format path and its required compatibility and PMC process. |
|
Cross-reference from #8986: budget-aware TaskData prefix splitting reaches the same unresolved contract. Gatekeeper reproduced both row-order corruption (fresh replacement Fragment IDs move an early rewrite behind the untouched suffix) and non-convergence ( |
|
No code change was made at 58b67b4. The #8986 evidence confirms the existing contract boundary rather than supplying a compatible implementation: this head preserves ID-sorted manifests and strictly bounds every changed Fragment identity, which can necessarily strand older eligible work, and the #8986 author likewise verified that prefix splitting is unsafe under that representation. Adopting ordered partial commits as a format feature or relaxing the strict budget/progress guarantees still requires explicit maintainer/PMC direction. |
|
Blocked: PR #8400 still requires an explicit maintainer/PMC decision on the bounded-compaction contract before this repair can advance. At remote head The #8986 contribution now has a verified App disposition, all prior actionable threads retain valid dispositions, formatting and all 145 focused compaction tests pass, all 38 current-head checks succeed, and a normal push verified the published head. No code-only change can select among these incompatible compatibility guarantees without maintainer authority. The smallest unblock is for a maintainer to confirm that strict bound and exclusion semantics may sacrifice compaction progress. Alternatively, explicitly permit suffix relabel/remap work beyond the bound, or approve the logical-order format path and its required compatibility and PMC process. |
Summary
Root cause
Compaction replacement fragments receive fresh IDs above the manifest high-water mark. The commit path globally sorts fragments by ID, so a replacement for an early range moved behind any untouched later fragments. Concurrently completed tasks could also arrive in a different order, and bounded compaction exposed the same defect repeatedly.
Fix
Compaction results are first ordered by their current source positions. Starting at the earliest rewritten range, the commit completes one ordered replacement suffix: real compaction outputs replace planned ranges, while untouched trailing fragments are represented by metadata-only replacements that keep their data files and receive fresh consecutive IDs. Deletion files are copied to the paths implied by the new fragment IDs, physical row-address indices are remapped, and stable-row-ID index coverage follows the relabeled fragments.
Stale distributed tasks recognize prior metadata-only relabels and rebase captured row addresses. Genuine source changes remain retryable conflicts, as do concurrent appends and row-adding updates that would invalidate the reserved suffix ordering. The commit boundary continues to require strictly increasing fragment IDs, so released readers and writers retain their existing representation and compatibility.
Validation
cargo fmt --all -- --checkcargo clippy --all --tests --benches -- -D warningscargo test -p lance dataset::optimize::tests -- --nocapture(117 passed)cargo test -p lance dataset::transaction::tests -- --nocapture(63 passed)cargo test -p lance io::commit::conflict_resolver::tests -- --nocapture(44 passed)cargo test -p lance test_check_fragment_ids_requires_sorted_order -- --nocapture(1 passed)cargo test -p lance test_compact_distributed -- --nocapture(4 passed)cargo test -p lance test_bounded_compaction_preserves_order_across_candidate_gap -- --nocapture(2 passed)make buildfrompython/Fixes #3465