datafusion_distributed/execution_plans/distributed_leaf.rs
1use crate::DistributedTaskContext;
2use datafusion::common::tree_node::TreeNodeRecursion;
3use datafusion::common::{Result, Statistics, exec_err, not_impl_err, plan_err};
4use datafusion::execution::{SendableRecordBatchStream, TaskContext};
5use datafusion::physical_expr::PhysicalExpr;
6use datafusion::physical_expr_common::metrics::MetricsSet;
7use datafusion::physical_plan::{
8 DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, StatisticsArgs,
9};
10use std::fmt::Formatter;
11use std::sync::Arc;
12
13/// Represents a leaf node ready to be distributed across N tasks, where the variant of the node
14/// belonging to each task is stored in a `Vec` of N positions.
15///
16/// While sending this plan over the wire to a remote worker, only the appropriate variant is sent.
17///
18/// This [ExecutionPlan] implementation is typically returned by
19/// [crate::TaskEstimator::scale_up_leaf_node], which will be called for scaling up a node for
20/// distribution. The process typically looks like this:
21///
22/// 1. The distributed planner calls [crate::TaskEstimator::scale_up_leaf_node] providing a leaf
23/// node and the amount of tasks in which it should be distributed:
24///
25/// ```text
26/// ┌──────────────┐
27/// │DataSourceExec│ + 3 tasks
28/// └──────────────┘
29/// ```
30///
31/// 2. The [crate::TaskEstimator] implementation, either user provided or a default one, returns
32/// a [DistributedLeafExec] adhering to this task count:
33///
34/// ```text
35/// ┌────────────────────────────────────────────────┐
36/// │ DistributedLeafExec │
37/// │ │
38/// │┌──────────────┐┌──────────────┐┌──────────────┐│
39/// ││DataSourceExec││DataSourceExec││DataSourceExec││
40/// ││ for task 0 ││ for task 1 ││ for task 2 ││
41/// │└──────────────┘└──────────────┘└──────────────┘│
42/// └────────────────────────────────────────────────┘
43/// ```
44///
45/// 3. The [crate::DistributedExec] node, upon being executed, will send the different variants of
46/// the leaf node to the respective workers, instead of sending the full [DistributedLeafExec]:
47///
48/// ```text
49/// ┌──────────────────┐┌──────────────────┐┌──────────────────┐
50/// │ Worker 0 ││ Worker 1 ││ Worker 2 │
51/// │ ││ ││ │
52/// │ ... ││ ... ││ ... │
53/// │ ││ ││ │
54/// │ ┌──────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │
55/// │ │ SomeExec │ ││ │ SomeExec │ ││ │ SomeExec │ │
56/// │ │ │ ││ │ │ ││ │ │ │
57/// │ └──────────────┘ ││ └──────────────┘ ││ └──────────────┘ │
58/// │ ┌──────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │
59/// │ │DataSourceExec│ ││ │DataSourceExec│ ││ │DataSourceExec│ │
60/// │ │ for task 0 │ ││ │ for task 1 │ ││ │ for task 2 │ │
61/// │ └──────────────┘ ││ └──────────────┘ ││ └──────────────┘ │
62/// └──────────────────┘└──────────────────┘└──────────────────┘
63/// ```
64///
65/// This way, the different workers get to execute different versions of the same plan, each
66/// handling its own range of non-overlapping data.
67///
68/// Note that every variant in `variants` must agree on the same schema and partition count with
69/// every other variant, but variants are **not** required to match the partition count of the
70/// `original` leaf plan. Variants can scale their partition count higher or lower as needed for
71/// worker execution.
72#[derive(Debug)]
73pub struct DistributedLeafExec {
74 pub(crate) original: Arc<dyn ExecutionPlan>,
75 pub(crate) properties: Arc<PlanProperties>,
76 pub(crate) variants: Vec<Arc<dyn ExecutionPlan>>,
77}
78
79impl DistributedLeafExec {
80 /// Builds a new [DistributedLeafExec] based on the provided original plan and its per-task
81 /// variants.
82 ///
83 /// Every variant must expose the same schema and partition count as every other variant.
84 /// Variants do not need to match the partition count of the `original` plan.
85 pub fn try_new(
86 original: Arc<dyn ExecutionPlan>,
87 variants: impl IntoIterator<Item = Arc<dyn ExecutionPlan>>,
88 ) -> Result<Self> {
89 let mut properties = None;
90 let variants = variants
91 .into_iter()
92 .map(|plan| {
93 let plan_properties = plan.properties();
94 let Some(prev) = &properties else {
95 properties = Some(Arc::clone(plan_properties));
96 return Ok(plan);
97 };
98 if prev.partitioning.partition_count()
99 != plan_properties.partitioning.partition_count()
100 {
101 return plan_err!("Different partition count where provided in two different variants of DistributedLeafExec")
102 }
103 if !prev.eq_properties.schema().eq(plan_properties.eq_properties.schema()) {
104 return plan_err!("Different schemas where provided in two different variants of DistributedLeafExec")
105 }
106
107 Ok(plan)
108 })
109 .collect::<Result<Vec<_>>>()?;
110
111 let Some(properties) = properties else {
112 return plan_err!("Empty list of variants was provided to DistributedLeafExec");
113 };
114
115 Ok(Self {
116 original,
117 properties,
118 variants,
119 })
120 }
121
122 /// The plan this leaf was built from (the leaf passed to
123 /// [crate::TaskEstimator::scale_up_leaf_node]). Useful for recognising which `DistributedLeafExec`
124 /// you are looking at — e.g. by downcasting it to your own leaf type — before inspecting its
125 /// [DistributedLeafExec::variants].
126 pub fn original(&self) -> &Arc<dyn ExecutionPlan> {
127 &self.original
128 }
129
130 /// The per-task variants, in task order: `variants()[i]` is the plan sent to task `i`. Useful
131 /// for inspecting per-task information (e.g. data locality) when routing tasks to workers via
132 /// [crate::TaskEstimator::route_tasks].
133 pub fn variants(&self) -> &[Arc<dyn ExecutionPlan>] {
134 &self.variants
135 }
136
137 /// Returns the variant belonging to provided task index.
138 pub(crate) fn to_task_specialized(&self, task_i: usize) -> Arc<dyn ExecutionPlan> {
139 Arc::clone(&self.variants[task_i])
140 }
141}
142
143impl DisplayAs for DistributedLeafExec {
144 fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
145 write!(f, "DistributedLeafExec: ")?;
146 self.original.fmt_as(t, f)
147 }
148}
149
150impl ExecutionPlan for DistributedLeafExec {
151 fn name(&self) -> &str {
152 "DistributedLeafExec"
153 }
154
155 fn properties(&self) -> &Arc<PlanProperties> {
156 &self.properties
157 }
158
159 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
160 vec![]
161 }
162
163 fn apply_expressions(
164 &self,
165 f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
166 ) -> Result<TreeNodeRecursion> {
167 self.original.apply_expressions(f)
168 }
169
170 fn with_new_children(
171 self: Arc<Self>,
172 children: Vec<Arc<dyn ExecutionPlan>>,
173 ) -> Result<Arc<dyn ExecutionPlan>> {
174 if !children.is_empty() {
175 return not_impl_err!("DistributedLeafExec does not accept children");
176 }
177 Ok(self)
178 }
179
180 fn execute(
181 &self,
182 partition: usize,
183 context: Arc<TaskContext>,
184 ) -> Result<SendableRecordBatchStream> {
185 let d_ctx = DistributedTaskContext::from_ctx(&context);
186 if d_ctx.task_count == 1 {
187 return self.original.execute(partition, context);
188 }
189
190 let Some(plan) = self.variants.get(d_ctx.task_index) else {
191 return exec_err!(
192 "Task index {} out of range for a per_task vector of length {}",
193 d_ctx.task_index,
194 self.variants.len()
195 );
196 };
197
198 plan.execute(partition, context)
199 }
200
201 fn metrics(&self) -> Option<MetricsSet> {
202 self.original.metrics()
203 }
204
205 fn statistics_from_inputs(
206 &self,
207 _input_stats: &[Arc<Statistics>],
208 args: &StatisticsArgs,
209 ) -> Result<Arc<Statistics>> {
210 // `original` is deliberately hidden from `children()` so this remains a distributed leaf.
211 // It is itself a leaf, so its statistics do not require child inputs.
212 self.original.statistics_from_inputs(&[], args)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::test_utils::plans::TestPlanBuilder;
220 use datafusion::common::tree_node::TreeNode;
221
222 #[tokio::test]
223 async fn exposes_original_leaf_expressions() -> Result<()> {
224 let plan = TestPlanBuilder::new()
225 .physical_plan(r#"SELECT * FROM weather WHERE "MinTemp" > 20"#)
226 .await;
227 let mut original = None;
228 plan.apply(|node| {
229 if node.children().is_empty() {
230 original = Some(Arc::clone(node));
231 return Ok(TreeNodeRecursion::Stop);
232 }
233 Ok(TreeNodeRecursion::Continue)
234 })?;
235 let original = original.expect("physical plan has a leaf");
236 let leaf = DistributedLeafExec::try_new(Arc::clone(&original), [original])?;
237
238 let mut expression_count = 0;
239 leaf.apply_expressions(&mut |_| {
240 expression_count += 1;
241 Ok(TreeNodeRecursion::Continue)
242 })?;
243
244 assert!(expression_count > 0);
245 Ok(())
246 }
247}