Skip to main content

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}