feat: cap spill merge fan-in#23066
Conversation
There was a problem hiding this comment.
@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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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. |
8315dce to
e564acd
Compare
|
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. |
|
@Kevin-Li-2025 |
|
@Kevin-Li-2025 |
|
Pushed f36a910 to address the remaining sqlite sqllogictest failure. The failed CI run showed the dynamic filter threshold as Local validation:
|
alamb
left a comment
There was a problem hiding this comment.
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" { |
There was a problem hiding this comment.
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" { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
I think limiting it to 100 files or something is probably reasonable. We can consider doing this as a follow on PR
| ) -> Vec<ConfigEntry> { | ||
| } | ||
|
|
||
| fn create_runtime_config_entries(values: RuntimeConfigValues) -> Vec<ConfigEntry> { |
There was a problem hiding this comment.
How about making this a method on RuntimeConfigValues -- like
impl RuntimeConfigValues {
fn into_config_entries(values: RuntimeConfigValues) -> Vec<ConfigEntry> {
..
}
..
}Signed-off-by: Kevin-Li-2025 <2242139@qq.com>
Signed-off-by: Kevin-Li-2025 <2242139@qq.com>
a32c55b to
33dc1b2
Compare
alamb
left a comment
There was a problem hiding this comment.
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 { |
| None, | ||
| Some("20M".to_owned()), | ||
| ) | ||
| RuntimeConfigValues { |
There was a problem hiding this comment.
this is a nice refactoring into a struct rather than a function
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?
datafusion.runtime.max_spill_merge_fan_in(0preserves the current unlimited behavior).SET/RESET/SHOW.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 --libcargo test -p datafusion-physical-plan spill_merge_fan_in --libcargo test -p datafusion --test core_integration test_max_spill_merge_fan_in_runtime_configcargo test -p datafusion-sqllogictest --test sqllogictests -- set_variable.sltcargo check -p datafusioncargo clippy -p datafusion-execution -p datafusion-physical-plan -p datafusion --lib -- -D warningscargo fmt --all -- --checkdev/update_config_docs.sh