Skip to main content

datafusion_distributed/worker/
impl_coordinator_channel.rs

1use crate::common::TreeNodeExt;
2use crate::events::{WorkerPlanRewriteEvent, WorkerPlanRewriteHandlers};
3use crate::execution_plans::SamplerExec;
4use crate::protocol::LocalWorkerContext;
5use crate::work_unit_feed::{RemoteWorkUnitFeedRegistry, set_work_unit_received_time};
6use crate::worker::task_data::TaskDataMetrics;
7use crate::{
8    CoordinatorToWorkerMsg, DistributedConfig, DistributedExt, DistributedTaskContext,
9    SetPlanRequest, TaskData, TaskMetrics, Worker, WorkerQueryContext, WorkerToCoordinatorMsg,
10};
11use datafusion::common::tree_node::TreeNodeRecursion;
12use datafusion::common::{DataFusionError, Result, exec_datafusion_err};
13use datafusion::execution::SessionStateBuilder;
14use datafusion::physical_plan::ExecutionPlan;
15use datafusion::prelude::SessionConfig;
16use futures::stream::{BoxStream, FuturesUnordered};
17use futures::{FutureExt, StreamExt, TryStreamExt};
18use http::HeaderMap;
19use std::sync::{Arc, OnceLock};
20use tokio::sync::oneshot;
21use tokio::sync::oneshot::Sender;
22
23impl Worker {
24    pub async fn coordinator_channel(
25        &self,
26        headers: HeaderMap,
27        request: SetPlanRequest,
28        stream: BoxStream<'static, Result<CoordinatorToWorkerMsg>>,
29    ) -> Result<BoxStream<'static, Result<WorkerToCoordinatorMsg>>> {
30        let key = request.task_key;
31
32        let entry = self
33            .task_data_entries
34            .get_with(key, async { Default::default() })
35            .await;
36
37        let mut remote_work_unit_feed_registry = RemoteWorkUnitFeedRegistry::default();
38        for decl in request.work_unit_feed_declarations {
39            remote_work_unit_feed_registry.add(decl.id, decl.partitions);
40        }
41
42        let (metrics_tx, metrics_rx) = oneshot::channel();
43        let mut load_info_rxs = vec![];
44
45        let task_data = || async {
46            let mut cfg = SessionConfig::default()
47                .with_extension(Arc::new(remote_work_unit_feed_registry.receivers))
48                .with_extension(Arc::new(DistributedTaskContext {
49                    task_index: request.task_key.task_number,
50                    task_count: request.task_count,
51                }))
52                .with_extension(Arc::new(LocalWorkerContext {
53                    local_worker: self.clone(),
54                    self_url: request.target_worker_url,
55                }))
56                .with_distributed_option_extension_from_headers::<DistributedConfig>(&headers)?;
57
58            let d_cfg = DistributedConfig::from_config_options(cfg.options())?;
59            let shuffle_batch_size = d_cfg.shuffle_batch_size;
60            let collect_metrics = d_cfg.collect_metrics;
61            if shuffle_batch_size != 0 {
62                cfg = cfg.with_batch_size(shuffle_batch_size);
63            }
64
65            let session_state = self
66                .session_builder
67                .build_session_state(WorkerQueryContext {
68                    builder: SessionStateBuilder::new()
69                        .with_default_features()
70                        .with_config(cfg)
71                        .with_runtime_env(Arc::clone(&self.runtime)),
72                    headers,
73                })
74                .await?;
75
76            let task_ctx = session_state.task_ctx();
77            let plan = request.plan.decode(&task_ctx)?;
78
79            let ev = WorkerPlanRewriteEvent {
80                plan,
81                session_config: session_state.config(),
82            };
83            let plan = WorkerPlanRewriteHandlers::handle(ev)?.plan;
84            load_info_rxs =
85                SamplerExec::kick_off_first_sampler(Arc::clone(&plan), Arc::clone(&task_ctx))?;
86
87            // Initialize partition count to the number of partitions in the stage
88            Ok::<_, DataFusionError>(TaskData {
89                base_plan: plan,
90                final_plan: Arc::new(OnceLock::new()),
91                task_ctx,
92                metrics_tx: match collect_metrics {
93                    true => Arc::new(std::sync::Mutex::new(Some(metrics_tx))),
94                    false => Arc::new(std::sync::Mutex::new(None)),
95                },
96                task_data_metrics: Arc::new(TaskDataMetrics::new(request.query_start_time_ns)),
97            })
98        };
99
100        let task_data_result = task_data().await.map_err(Arc::new);
101
102        entry
103            .write(task_data_result.clone())
104            .map_err(|e| exec_datafusion_err!("{e}"))?;
105
106        let task_data = task_data_result.map_err(DataFusionError::Shared)?;
107
108        // Continue reading remaining messages (work unit feed data) in the background.
109        let mut work_unit_senders = Some(remote_work_unit_feed_registry.senders);
110        let task_data_entries = Arc::clone(&self.task_data_entries);
111
112        // This tokio task takes ownership of the `oneshot::Sender<pb::TaskMetrics>` that keeps
113        // alive the worker->coordinator stream. as soon as this task ends, the runtime metrics
114        // are send back and the worker->coordinator stream ends. The flow is the following:
115        // 1. The query ends normally, as all Arrow RecordBatches are already streamed.
116        // 2. In DistributedExec::execute(), the end query guard is dropped.
117        // 3. In StageCoordinator::send_plan_task(), `end_stream_notifier` fires and the
118        //    coordinator->worker channel is gracefully ended.
119        // 4. The coordinator->worker channel EOS is received by this same function, ending the
120        //    while loop inside this `tokio::spawn` below.
121        // 5. The metrics are send back in the worker->coordinator channel, and then that channel
122        //    is closed.
123        #[allow(clippy::disallowed_methods)]
124        tokio::spawn(async move {
125            let mut stream = stream.map_ok(set_work_unit_received_time);
126            while let Some(Ok(msg)) = stream.next().await {
127                match msg {
128                    CoordinatorToWorkerMsg::WorkUnitBatch(work_unit_batch) => {
129                        let Some(work_unit_senders) = work_unit_senders.as_mut() else {
130                            continue;
131                        };
132                        for wu in work_unit_batch.batch {
133                            let id = wu.id;
134                            let partition = wu.partition;
135                            let Some(tx) = work_unit_senders.get(&(wu.id, partition)) else {
136                                continue;
137                            };
138                            if tx.send(Ok(wu)).is_err() {
139                                // Channel closed, this sender needs to be dropped, as none will ever
140                                // be listening on the other side.
141                                work_unit_senders.remove(&(id, partition));
142                                continue;
143                            }
144                        }
145                    }
146                    CoordinatorToWorkerMsg::WorkUnitEos => {
147                        // No further work unit message will be received here, so drop all the
148                        // sender sides so that receiver sides see an EOS upon draining the
149                        // remaining messages.
150                        //
151                        // The [WorkUnitEos] message just applies work units, and it's not a global
152                        // EOS signal for the coordinator->worker stream, as there might be more
153                        // messages of different nature in that stream.
154                        let _ = work_unit_senders.take();
155                    }
156                }
157            }
158
159            let metrics_tx = task_data.metrics_tx.lock().unwrap().take();
160            if let Some(Ok(plan)) = task_data.final_plan.get() {
161                let d_ctx = DistributedTaskContext {
162                    task_index: key.task_number,
163                    task_count: request.task_count,
164                };
165                let task_data_metrics = &task_data.task_data_metrics;
166                task_data_metrics.mark_execution_finished();
167                if let Some(metrics_tx) = metrics_tx {
168                    send_metrics_via_channel(metrics_tx, plan, d_ctx, task_data_metrics);
169                }
170            }
171            task_data_entries.invalidate(&key).await
172        });
173
174        let load_info_stream = FuturesUnordered::from_iter(load_info_rxs)
175            .filter_map(async |load_info_or_channel_dropped| {
176                // This error can only happen if the LoadInfo sender was dropped, which is fine.
177                let load_info = load_info_or_channel_dropped.ok()?;
178                Some(WorkerToCoordinatorMsg::LoadInfo(load_info))
179            })
180            .chain(futures::stream::once(async move {
181                WorkerToCoordinatorMsg::LoadInfoEos
182            }));
183
184        // Stream back metrics when the coordinator channel reaches EOS. At that point the
185        // coordinator has closed the query-scoped request stream, so any remaining task state can
186        // be finalized even if some partition streams were not dropped through the normal path.
187        let metrics_stream = metrics_rx.into_stream();
188        let metrics_stream = metrics_stream.filter_map(async |task_metrics_or_channel_dropped| {
189            let task_metrics = task_metrics_or_channel_dropped.ok()?;
190            Some(WorkerToCoordinatorMsg::TaskMetrics(task_metrics))
191        });
192
193        Ok(futures::stream::select(load_info_stream, metrics_stream)
194            .map(Ok)
195            .boxed())
196    }
197}
198
199/// Collects metrics from the plan in pre-order traversal order and sends them via the
200/// coordinator channel oneshot.
201fn send_metrics_via_channel(
202    metrics_tx: Sender<TaskMetrics>,
203    plan: &Arc<dyn ExecutionPlan>,
204    dt_ctx: DistributedTaskContext,
205    task_data_metrics: &Arc<TaskDataMetrics>,
206) {
207    let mut pre_order_plan_metrics = vec![];
208    let _ = plan.apply_with_dt_ctx(dt_ctx, |node, _| {
209        pre_order_plan_metrics.push(node.metrics().unwrap_or_default());
210        Ok(TreeNodeRecursion::Continue)
211    });
212
213    // Ignore send errors — the coordinator channel may have been dropped (e.g. query cancelled).
214    let _ = metrics_tx.send(TaskMetrics {
215        pre_order_plan_metrics,
216        task_metrics: task_data_metrics.to_metrics_set(),
217    });
218}