Uh oh!
There was an error while loading. Please reload this page.
feat: BufferExec waits for hash join dynamic filters before buffering - #21350
feat: BufferExec waits for hash join dynamic filters before buffering#21350LiaCastaneda wants to merge 7 commits into
Conversation
Adds ProducerKind to DynamicFilterPhysicalExpr to distinguish between hash join (safe to wait), TopK, and Aggregate (would deadlock) producers. BufferExec now walks its child subtree at execution time, collects any HashJoin dynamic filters, and waits for them to complete before the background task begins polling the input stream. This allows scans below BufferExec to benefit from dynamic filters rather than being eagerly read before the filter is populated. A new session config option `hash_join_buffering_dynamic_filter_wait_ms` controls the wait behavior (0 = disabled, usize::MAX = wait indefinitely, any other value = timeout in ms). Defaults to usize::MAX. Closesapache#20778
| @@ -684,6 +684,17 @@ config_namespace! { | |||
| /// | |||
| /// Disabled by default, set to a number greater than 0 for enabling it. | |||
| pub hash_join_buffering_capacity: usize, default = 0 | |||
There was a problem hiding this comment.
Should we enable this by default? I think now it should be safe
There was a problem hiding this comment.
In my opinion it should be opt-in as it increases memory usage and in future we want to make parallelism / pipelining scheduling probably more explicit.
| if let Some(df) = expr.as_any().downcast_ref::<DynamicFilterPhysicalExpr>() | ||
| && df.producer_kind() == ProducerKind::HashJoin | ||
| { | ||
| filters.push(df.clone()); |
There was a problem hiding this comment.
This clone should be cheap
| let mut filters = vec![]; | ||
| let _ = plan.apply_expressions(&mut |expr| { | ||
| if let Some(df) = expr.as_any().downcast_ref::<DynamicFilterPhysicalExpr>() | ||
| && df.producer_kind() == ProducerKind::HashJoin |
There was a problem hiding this comment.
We need to know who the producer is, otherwise if we wait on a dynamic filter from a TopK (or AggregateExec) we can enter a deadlock.
496a4dd to
0c4469fCompare…ble parquet pushdown_filters in tests
…che#22437) ## Which issue does this PR close? - Reverts apache#20337 - Addresses concerns raised in apache#22415 - Closesapache#22415 ## Rationale for this change `ExecutionPlan::apply_expressions()` was added in apache#20337 with no default implementation, forcing every custom `ExecutionPlan`, `FileSource`, and `DataSource` implementor to add the method as part of upgrading to DataFusion 54. As discussed on apache#22415, per @LiaCastaneda and @adriangb the method is not yet called from anywhere in DataFusion and the originally intended use (dynamic-filter discovery/serialization for distributed scenarios) is blocked on other in-progress work (apache#20009, apache#21350). The combined effect on downstream users is a required code change with no immediate benefit, and ambiguity about what a "correct" implementation even means today (e.g. is returning `Ok(TreeNodeRecursion::Continue)` is safe right now but becomes incorrect as soon as the method starts being used by an optimizer pass?. The plan agreed in the discussion is to remove the API from the 54.0 release and re-add it together with the concrete consumer that needs it. cc @adriangb@LiaCastaneda@milenkovicm. ## What changes are included in this PR? `git revert -m 1` of the merge commit, with the following manual conflict resolutions and follow-ups: ## Are these changes tested? By CI ## Are there any user-facing changes? Yes -- this removes the new public API: - `ExecutionPlan::apply_expressions` - `FileSource::apply_expressions` - `DataSource::apply_expressions` These were only added in 54 and are not yet released. Custom implementors no longer need to implement these methods.
Thank you for your contribution. Unfortunately, this pull request is stale because it has been open 60 days with no activity. Please remove the stale label or comment or this will be closed in 7 days. |
BufferExec now walks its child subtree at execution time, collects any HashJoin dynamic filters, and waits for them to complete before the background task begins polling the input stream. This allows scans below
BufferExecto benefit from dynamic filters rather than being eagerly read before the filter is populated.Which issue does this PR close?
Closes#20778
Rationale for this change
BufferExec was introduced to eagerly buffer the probe side of hash joins (instead of waiting for the build to fully complete) but it starts buffering before the build side finishes, so dynamic filters produced by the hash join are not yet populated when the probe-side scan executes. This PR makes BufferExec wait for those filters before starting to buffer, allowing scans to still benefir from dynamic filtering.
What changes are included in this PR?
Adds a ProducerKind enum to
DynamicFilterPhysicalExprto distinguish filter producers (HashJoin, TopK, Aggregate). BufferExec now collects HashJoin dynamic filters from its child subtree at execution time and waits for them to be populated before starting to buffer, with a configurable timeout viahash_join_buffering_dynamic_filter_wait_ms.TopKandAggregatefilters are intentionally skipped to avoid deadlocks, since those filters are populated incrementally as rows flow through the plan.Are these changes tested?
Yes, tests verify correct results when waiting for hash join dynamic filters and confirm no deadlock occurs when a TopK node appears below a hash join on the probe side.
Are there any user-facing changes?
A new option was added:
hash_join_buffering_dynamic_filter_wait_ms