Skip to content

feat: cap spill merge fan-in#23066

Merged
alamb merged 6 commits into
apache:mainfrom
Kevin-Li-2025:feat/spill-merge-fan-in
Jul 3, 2026
Merged

feat: cap spill merge fan-in#23066
alamb merged 6 commits into
apache:mainfrom
Kevin-Li-2025:feat/spill-merge-fan-in

Conversation

@Kevin-Li-2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

External sort merge phases currently select spill files based only on memory reservation. With many small spills, a single phase can open enough files to exceed the process file-descriptor limit.

What changes are included in this PR?

  • Add datafusion.runtime.max_spill_merge_fan_in (0 preserves the current unlimited behavior).
  • Clamp non-zero values to at least 2 during merge selection so each pass makes progress.
  • Support builder configuration and dynamic SQL SET / RESET / SHOW.
  • Add unit, runtime SQL, SQLLogicTest, information schema, and generated documentation coverage.

Are there any user-facing changes?

Users can cap the number of spill files opened in one external merge pass. The default remains unchanged.

How was this change tested?

  • cargo test -p datafusion-execution test_max_spill_merge_fan_in_builder_and_dynamic_update --lib
  • cargo test -p datafusion-physical-plan spill_merge_fan_in --lib
  • cargo test -p datafusion --test core_integration test_max_spill_merge_fan_in_runtime_config
  • cargo test -p datafusion-sqllogictest --test sqllogictests -- set_variable.slt
  • cargo check -p datafusion
  • cargo clippy -p datafusion-execution -p datafusion-physical-plan -p datafusion --lib -- -D warnings
  • cargo fmt --all -- --check
  • dev/update_config_docs.sh

@github-actions github-actions Bot added documentation Improvements or additions to documentation core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) execution Related to the execution crate physical-plan Changes to the physical-plan crate labels Jun 21, 2026

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kevin-Li-2025
Looks good to me. I left one small suggestion that could make the test coverage a bit more direct.

let mut total_needed: usize = 0;

for spill in &self.sorted_spill_files {
if number_of_spills_to_read_for_current_phase >= max_spill_files {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice coverage on the helper cases for 0/1/2/N. One extra test could make this even stronger: build multiple SortedSpillFiles with max_spill_merge_fan_in = 2 and assert that a merge phase selects only two spill inputs. That would guard the FD-limit regression more directly than the current config and helper coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e564acd: added spill_merge_phase_respects_configured_fan_in, which builds four SortedSpillFiles with max_spill_merge_fan_in = 2 and asserts one merge phase selects exactly two spill inputs while leaving two for the next phase.

@Kevin-Li-2025

Copy link
Copy Markdown
Contributor Author

Addressed the test coverage suggestion in 8315dce. I added a focused unit test that builds four SortedSpillFile entries with max_spill_merge_fan_in = 2 and asserts that one merge phase selects/drains only two spill inputs, leaving the remaining two for a later phase.\n\nLocal validation: git diff --check passed. I could not run cargo fmt/test in this sandbox because rustup tries to write under ~/.rustup/update-hashes, which is not writable here, and a temporary minimal toolchain download stalled before completion.

@Kevin-Li-2025 Kevin-Li-2025 force-pushed the feat/spill-merge-fan-in branch from 8315dce to e564acd Compare June 25, 2026 15:42
@Kevin-Li-2025

Copy link
Copy Markdown
Contributor Author

Rebased onto current apache/datafusion main and resolved the conflict in disk_manager.rs by keeping the current builder-only DiskManager construction path; the spill fan-in setting is still wired through DiskManagerBuilder. New head: e564acd. Validation: git diff --check origin/main...HEAD.

@kosiew

kosiew commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@Kevin-Li-2025
Can you resolve the cargo fmt errors?

@kosiew

kosiew commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@Kevin-Li-2025
Can you resolve the CI error?

@Kevin-Li-2025

Copy link
Copy Markdown
Contributor Author

Pushed f36a910 to address the remaining sqlite sqllogictest failure.

The failed CI run showed the dynamic filter threshold as column1@0 > 3, while the same focused test can produce column1@0 > 4 locally. That matches the existing note in the test: the partial aggregate dynamic-filter update races with scan progress under parallel execution. I changed this assertion to keep checking the stable invariant (DynamicFilter is present, dynamic row-group pruning is eligible, and metrics are suppressed) while ignoring the volatile threshold value and file-group text.

Local validation:

  • cargo test -p datafusion-sqllogictest --test sqllogictests -- push_down_filter_regression --include-sqlite
  • cargo fmt --all -- --check

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jun 30, 2026

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Kevin-Li-2025 and @kosiew -- I had some code structure questions, but the idea looks good to me


let mut state = self.state.write();

if key == "max_spill_merge_fan_in" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't this follow the same pattern as the other variables (aka in the match statement below)?


let mut state = self.state.write();

if key == "max_spill_merge_fan_in" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto -- I think this should follow the same pattern as the other variabales

use datafusion_common::human_readable_size;

pub const DEFAULT_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; // 100GB
pub const DEFAULT_MAX_SPILL_MERGE_FAN_IN: usize = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think limiting it to 100 files or something is probably reasonable. We can consider doing this as a follow on PR

Comment thread datafusion/execution/src/runtime_env.rs Outdated
) -> Vec<ConfigEntry> {
}

fn create_runtime_config_entries(values: RuntimeConfigValues) -> Vec<ConfigEntry> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about making this a method on RuntimeConfigValues -- like

impl RuntimeConfigValues {
  fn into_config_entries(values: RuntimeConfigValues) -> Vec<ConfigEntry> {
    ..
  }
..
}

@Kevin-Li-2025 Kevin-Li-2025 force-pushed the feat/spill-merge-fan-in branch from a32c55b to 33dc1b2 Compare July 1, 2026 07:36
@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Jul 2, 2026
@kosiew kosiew requested a review from alamb July 3, 2026 05:57

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me -- thank you @kosiew and @Kevin-Li-2025

/// entries to avoid duplication between `RuntimeEnv::config_entries()` and
/// `RuntimeEnvBuilder::entries()`.
fn create_runtime_config_entries(
struct RuntimeConfigValues {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

None,
Some("20M".to_owned()),
)
RuntimeConfigValues {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a nice refactoring into a struct rather than a function

@alamb alamb enabled auto-merge July 3, 2026 14:20
@alamb alamb added this pull request to the merge queue Jul 3, 2026
Merged via the queue into apache:main with commit b7e7b51 Jul 3, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate documentation Improvements or additions to documentation execution Related to the execution crate physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a configurable cap on spill-file merge fan-in (max open files during external merge)

3 participants