Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 46 additions & 37 deletions datafusion/physical-plan/src/joins/nested_loop_join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,22 +681,23 @@ impl ExecutionPlan for NestedLoopJoinExec {
// Determine if OOM fallback to memory-limited mode is possible.
// Conditions:
// 1. Disk manager supports temp files (needed for spilling).
// 2. FULL join with multiple right partitions is not yet supported
// in the fallback path. FULL join needs to track BOTH left-side
// matches (for unmatched left rows) AND right-side matches (for
// unmatched right rows). The fallback path builds a per-partition
// `JoinLeftData` with `probe_threads_counter == 1`, so each
// partition emits unmatched left rows based only on its own
// right-side matches, producing incorrect duplicate output for
// left rows that match in another partition. Other join types
// that need only one-sided final emission (LEFT, LEFT SEMI,
// LEFT ANTI, LEFT MARK) have a similar latent issue in the
// fallback path which predates this change; tracking is out of
// scope for this PR.
let full_join_multi_partition =
matches!(self.join_type, JoinType::Full) && right_partition_count > 1;
// 2. Join types whose final emission reads the visited-left bitmap
// (LEFT, LEFT SEMI, LEFT ANTI, LEFT MARK, FULL) need that bitmap
// complete across every probe partition. The fallback path builds a
// per-partition `JoinLeftData` with `probe_threads_counter == 1`, so
// with more than one right partition each partition emits from a
// bitmap that only saw its own right rows: unmatched rows are
// emitted once per partition, and rows matched only in another
// partition are emitted as unmatched. Refusing the fallback turns
// those wrong results into a plain ResourcesExhausted error.
// Right-side emission types are safe because each partition owns
// its right rows exclusively. Cross-partition coordination of the
// left bitmap is tracked in
// https://github.com/apache/datafusion/issues/22038.
let left_emission_multi_partition =
need_produce_result_in_final(self.join_type) && right_partition_count > 1;
let spill_state = if context.runtime_env().disk_manager.tmp_files_enabled()
&& !full_join_multi_partition
&& !left_emission_multi_partition
{
SpillState::Pending {
left_plan: Arc::clone(&self.left),
Expand Down Expand Up @@ -3842,12 +3843,10 @@ pub(crate) mod tests {

// Join types that support memory-limited fallback should succeed
// even under tight memory limits (they spill to disk instead of OOM).
// Right-side emission types are safe with multiple right partitions
// because each partition owns its right rows exclusively.
let fallback_join_types = vec![
JoinType::Inner,
JoinType::Left,
JoinType::LeftSemi,
JoinType::LeftAnti,
JoinType::LeftMark,
JoinType::Right,
JoinType::RightSemi,
JoinType::RightAnti,
Expand All @@ -3872,24 +3871,34 @@ pub(crate) mod tests {
.await?;
}

// FULL JOIN with multiple right partitions is intentionally not
// supported in the fallback path yet (cross-partition left-bitmap
// coordination is missing). It should still OOM under tight memory.
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(100, 1.0)
.build_arc()?;
let task_ctx = TaskContext::default().with_runtime(runtime);
let task_ctx = Arc::new(task_ctx);
let err = multi_partitioned_join_collect(
Arc::clone(&left),
Arc::clone(&right),
&JoinType::Full,
Some(filter.clone()),
task_ctx,
)
.await
.unwrap_err();
assert_contains!(err.to_string(), "Resources exhausted");
// Join types whose final emission reads the visited-left bitmap are
// intentionally not supported in the fallback path with multiple right
// partitions (cross-partition left-bitmap coordination is missing;
// per-partition bitmaps emit wrong rows). They must OOM cleanly instead.
let gated_join_types = vec![
JoinType::Left,
JoinType::LeftSemi,
JoinType::LeftAnti,
JoinType::LeftMark,
JoinType::Full,
];
for join_type in &gated_join_types {
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(100, 1.0)
.build_arc()?;
let task_ctx = TaskContext::default().with_runtime(runtime);
let task_ctx = Arc::new(task_ctx);
let err = multi_partitioned_join_collect(
Arc::clone(&left),
Arc::clone(&right),
join_type,
Some(filter.clone()),
task_ctx,
)
.await
.unwrap_err();
assert_contains!(err.to_string(), "Resources exhausted");
}

Ok(())
}
Expand Down