Skip to content

Commit d12d12b

Browse files
committed
Merge branch 'main' into gabrielmusat/refactor-distributed-planning
# Conflicts: # src/execution_plans/distributed.rs # src/execution_plans/network_broadcast.rs # src/execution_plans/network_coalesce.rs # src/execution_plans/network_shuffle.rs # src/metrics/task_metrics_rewriter.rs
2 parents 7626c71 + 630ec66 commit d12d12b

23 files changed

Lines changed: 354 additions & 370 deletions

benchmarks/cdk/bin/worker.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
183183
physical,
184184
DistributedMetricsFormat::PerTask,
185185
)
186+
.await
186187
.map_err(err)?;
187188
let plan = display_plan_ascii(physical.as_ref(), true);
188189
drop(task);

console/examples/tpcds_runner.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ async fn run_single_query(
189189
let batches = stream.try_collect::<Vec<_>>().await?;
190190
if explain_analyze {
191191
let output =
192-
datafusion_distributed::explain_analyze(plan, DistributedMetricsFormat::Aggregated)?;
192+
datafusion_distributed::explain_analyze(plan, DistributedMetricsFormat::Aggregated)
193+
.await?;
193194
println!("{output}");
194195
}
195196
Ok(batches)

src/execution_plans/benchmarks/shuffle_bench.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,6 @@ impl ShuffleFixture {
229229
)),
230230
input_stage: input_stage.clone(),
231231
worker_connections: WorkerConnectionPool::new(self.bench.producer_tasks),
232-
metrics_collection: Arc::new(Default::default()),
233232
};
234233
let task_ctx = Arc::new(task_ctx_with_extension(
235234
&self.task_ctx,

src/execution_plans/benchmarks/transport_bench.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ impl TransportFixture {
281281
worker_connections: crate::worker::WorkerConnectionPool::new(
282282
self.bench.producer_tasks,
283283
),
284-
metrics_collection: Arc::new(Default::default()),
285284
};
286285
let task_ctx = Arc::new(task_ctx_with_extension(
287286
&self.task_ctx,

src/execution_plans/distributed.rs

Lines changed: 141 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ use crate::networking::get_distributed_worker_resolver;
66
use crate::passthrough_headers::get_passthrough_headers;
77
use crate::protobuf::{DistributedCodec, tonic_status_to_datafusion_error};
88
use crate::stage::{LocalStage, RemoteStage, Stage};
9+
use crate::worker::generated::worker as pb;
910
use crate::worker::generated::worker::set_plan_request::WorkUnitFeedDeclaration;
1011
use crate::worker::generated::worker::{
11-
CoordinatorToWorkerMsg, SetPlanRequest, TaskKey, WorkUnit, coordinator_to_worker_msg::Inner,
12+
CoordinatorToWorkerMsg, SetPlanRequest, TaskKey, WorkUnit, WorkerToCoordinatorMsg,
13+
coordinator_to_worker_msg::Inner, worker_to_coordinator_msg,
1214
};
1315
use crate::{
1416
DISTRIBUTED_DATAFUSION_TASK_ID_LABEL, DistributedConfig, DistributedTaskContext,
1517
DistributedWorkUnitFeedContext, WorkerResolver, get_distributed_channel_resolver,
1618
};
19+
use datafusion::common::HashMap;
1720
use datafusion::common::instant::Instant;
1821
use datafusion::common::runtime::JoinSet;
19-
use datafusion::common::tree_node::{Transformed, TreeNode};
22+
use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
2023
use datafusion::common::{Result, exec_err};
2124
use datafusion::common::{exec_datafusion_err, internal_datafusion_err};
2225
use datafusion::error::DataFusionError;
@@ -41,12 +44,46 @@ use std::sync::Mutex;
4144
use std::sync::atomic::{AtomicU64, Ordering};
4245
use std::time::Duration;
4346
use tokio::sync::mpsc::UnboundedSender;
47+
use tokio::sync::watch;
4448
use tokio_stream::wrappers::UnboundedReceiverStream;
4549
use tonic::Request;
4650
use tonic::metadata::MetadataMap;
4751
use url::Url;
4852
use uuid::Uuid;
4953

54+
/// Stores the metrics collected from all worker tasks, and notifies waiters when new entries arrive.
55+
#[derive(Debug, Clone)]
56+
pub struct MetricsStore {
57+
tx: watch::Sender<HashMap<TaskKey, Vec<pb::MetricsSet>>>,
58+
rx: watch::Receiver<HashMap<TaskKey, Vec<pb::MetricsSet>>>,
59+
}
60+
61+
impl MetricsStore {
62+
fn new() -> Self {
63+
let (tx, rx) = watch::channel(HashMap::new());
64+
Self { tx, rx }
65+
}
66+
67+
pub fn insert(&self, key: TaskKey, metrics: Vec<pb::MetricsSet>) {
68+
self.tx.send_modify(|map| {
69+
map.insert(key, metrics);
70+
});
71+
}
72+
73+
pub fn get(&self, key: &TaskKey) -> Option<Vec<pb::MetricsSet>> {
74+
self.rx.borrow().get(key).cloned()
75+
}
76+
77+
#[cfg(test)]
78+
pub(crate) fn from_entries(
79+
entries: impl IntoIterator<Item = (TaskKey, Vec<pb::MetricsSet>)>,
80+
) -> Self {
81+
let map: HashMap<_, _> = entries.into_iter().collect();
82+
let (tx, rx) = watch::channel(map);
83+
Self { tx, rx }
84+
}
85+
}
86+
5087
/// [ExecutionPlan] that executes the inner plan in distributed mode.
5188
/// Before executing it, two modifications are lazily performed on the plan:
5289
/// 1. Assigns worker URLs to all the stages. A random set of URLs are sampled from the
@@ -58,6 +95,7 @@ pub struct DistributedExec {
5895
pub plan: Arc<dyn ExecutionPlan>,
5996
pub prepared_plan: Arc<Mutex<Option<Arc<dyn ExecutionPlan>>>>,
6097
metrics: ExecutionPlanMetricsSet,
98+
pub task_metrics: Arc<MetricsStore>,
6199
}
62100

63101
struct PreparedPlan {
@@ -71,9 +109,41 @@ impl DistributedExec {
71109
plan,
72110
prepared_plan: Arc::new(Mutex::new(None)),
73111
metrics: ExecutionPlanMetricsSet::new(),
112+
task_metrics: Arc::new(MetricsStore::new()),
74113
}
75114
}
76115

116+
/// Waits until all worker tasks have reported their metrics back via the coordinator channel.
117+
///
118+
/// Metrics are delivered asynchronously after query execution completes, so callers that need
119+
/// complete metrics (e.g. for observability or display) should await this before inspecting
120+
/// [`Self::task_metrics`] or calling [`rewrite_distributed_plan_with_metrics`].
121+
///
122+
/// [`rewrite_distributed_plan_with_metrics`]: crate::rewrite_distributed_plan_with_metrics
123+
pub async fn wait_for_metrics(&self) {
124+
let mut expected_keys: Vec<TaskKey> = Vec::new();
125+
let _ = self.plan.apply(|plan| {
126+
if let Some(boundary) = plan.as_network_boundary() {
127+
let stage = boundary.input_stage();
128+
for i in 0..stage.task_count() {
129+
expected_keys.push(TaskKey {
130+
query_id: serialize_uuid(&stage.query_id()),
131+
stage_id: stage.num() as u64,
132+
task_number: i as u64,
133+
});
134+
}
135+
}
136+
Ok(TreeNodeRecursion::Continue)
137+
});
138+
if expected_keys.is_empty() {
139+
return;
140+
}
141+
let mut rx = self.task_metrics.rx.clone();
142+
let _ = rx
143+
.wait_for(|map| expected_keys.iter().all(|key| map.contains_key(key)))
144+
.await;
145+
}
146+
77147
/// Returns the plan which is lazily prepared on execute() and actually gets executed.
78148
/// It is updated on every call to execute(). Returns an error if .execute() has not been called.
79149
pub(crate) fn prepared_plan(&self) -> Result<Arc<dyn ExecutionPlan>, DataFusionError> {
@@ -93,6 +163,8 @@ impl DistributedExec {
93163
/// network call feeding the subplan is necessary.
94164
/// 3. In each network boundary, set the input plan to `None`. That way, network boundaries
95165
/// become nodes without children and traversing them will not go further down in.
166+
/// 4. Spawn a background task per worker that waits for the worker to finish and collects
167+
/// its metrics into [DistributedExec::task_metrics] via the coordinator channel.
96168
fn prepare_plan(&self, ctx: &Arc<TaskContext>) -> Result<PreparedPlan> {
97169
let worker_resolver = get_distributed_worker_resolver(ctx.session_config())?;
98170
let codec = DistributedCodec::new_combined_with_user(ctx.session_config());
@@ -123,8 +195,13 @@ impl DistributedExec {
123195
return exec_err!("Input stage from network boundary was not in Local state");
124196
};
125197

126-
let mut spawner =
127-
CoordinatorToWorkerTaskSpawner::new(stage, &metrics, &codec, &mut join_set)?;
198+
let mut spawner = CoordinatorToWorkerTaskSpawner::new(
199+
stage,
200+
&metrics,
201+
&self.task_metrics,
202+
&codec,
203+
&mut join_set,
204+
)?;
128205

129206
// Right now, we assign random workers to tasks. This might change in the future.
130207
let start_idx = rand::rng().random_range(0..urls.len());
@@ -135,7 +212,8 @@ impl DistributedExec {
135212
workers.push(url.clone());
136213
// Spawns the task that feeds this subplan to this worker. There will be as
137214
// many as this spawned tasks as workers.
138-
let tx = spawner.send_plan_task(Arc::clone(ctx), i, url)?;
215+
let (tx, worker_rx) = spawner.send_plan_task(Arc::clone(ctx), i, url)?;
216+
spawner.metrics_collection_task(i, worker_rx);
139217
spawner.work_unit_feed_task(Arc::clone(ctx), i, tx)?;
140218
}
141219

@@ -185,6 +263,7 @@ impl ExecutionPlan for DistributedExec {
185263
plan: require_one_child(&children)?,
186264
prepared_plan: self.prepared_plan.clone(),
187265
metrics: self.metrics.clone(),
266+
task_metrics: Arc::clone(&self.task_metrics),
188267
}))
189268
}
190269

@@ -254,13 +333,17 @@ struct CoordinatorToWorkerMetrics {
254333
/// - Building tasks that communicate a serialized plan to multiple workers for further execution.
255334
/// - Building tasks that stream partition feeds from local [WorkUnitFeedExec] nodes to their
256335
/// remote counterparts.
336+
type WorkerResponseRx =
337+
tokio::sync::mpsc::UnboundedReceiver<Result<WorkerToCoordinatorMsg, tonic::Status>>;
338+
257339
struct CoordinatorToWorkerTaskSpawner<'a> {
258340
plan: &'a Arc<dyn ExecutionPlan>,
259341
plan_proto: Vec<u8>,
260342
query_id: Uuid,
261343
stage_id: usize,
262344
task_count: usize,
263345
metrics: &'a CoordinatorToWorkerMetrics,
346+
task_metrics: &'a Arc<MetricsStore>,
264347
join_set: &'a mut JoinSet<Result<()>>,
265348
}
266349

@@ -270,6 +353,7 @@ impl<'a> CoordinatorToWorkerTaskSpawner<'a> {
270353
fn new(
271354
stage: &'a LocalStage,
272355
metrics: &'a CoordinatorToWorkerMetrics,
356+
task_metrics: &'a Arc<MetricsStore>,
273357
codec: &'a dyn PhysicalExtensionCodec,
274358
join_set: &'a mut JoinSet<Result<()>>,
275359
) -> Result<Self> {
@@ -283,18 +367,20 @@ impl<'a> CoordinatorToWorkerTaskSpawner<'a> {
283367
stage_id: stage.num,
284368
task_count: stage.tasks,
285369
metrics,
370+
task_metrics,
286371
join_set,
287372
})
288373
}
289374

290-
/// Instantiates and returns the task sends a serialized plan to specific worker. The returned
291-
/// task is just a future that does nothing unless polled.
375+
/// Sends a serialized plan to a specific worker and sets up the bidirectional gRPC stream.
376+
/// Returns the sender for outbound coordinator-to-worker messages and the receiver for
377+
/// inbound worker-to-coordinator messages.
292378
fn send_plan_task(
293379
&mut self,
294380
ctx: Arc<TaskContext>,
295381
task_i: usize,
296382
url: Url,
297-
) -> Result<UnboundedSender<CoordinatorToWorkerMsg>> {
383+
) -> Result<(UnboundedSender<CoordinatorToWorkerMsg>, WorkerResponseRx)> {
298384
let d_cfg = DistributedConfig::from_config_options(ctx.session_config().options())?;
299385
/// Searches recursively for nodes exposing [crate::WorkUnitFeed]s, and executes their
300386
/// feeds, keeping into account that some of them might be executed within a
@@ -346,21 +432,25 @@ impl<'a> CoordinatorToWorkerTaskSpawner<'a> {
346432
&mut work_unit_feed_declarations,
347433
);
348434

435+
let task_key = TaskKey {
436+
query_id: serialize_uuid(&self.query_id),
437+
stage_id: self.stage_id as u64,
438+
task_number: task_i as u64,
439+
};
349440
let msg = CoordinatorToWorkerMsg {
350441
inner: Some(Inner::SetPlanRequest(SetPlanRequest {
351442
plan_proto: self.plan_proto.clone(),
352443
task_count: self.task_count as u64,
353-
task_key: Some(TaskKey {
354-
query_id: serialize_uuid(&self.query_id),
355-
stage_id: self.stage_id as u64,
356-
task_number: task_i as u64,
357-
}),
444+
task_key: Some(task_key.clone()),
358445
work_unit_feed_declarations,
359446
})),
360447
};
361448
let plan_size = self.plan_proto.len();
362449

363-
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
450+
let (coordinator_to_worker_tx, coordinator_to_worker_rx) =
451+
tokio::sync::mpsc::unbounded_channel();
452+
let (worker_to_coordinator_tx, worker_to_coordinator_rx) =
453+
tokio::sync::mpsc::unbounded_channel();
364454

365455
let channel_resolver = get_distributed_channel_resolver(ctx.as_ref());
366456

@@ -370,23 +460,57 @@ impl<'a> CoordinatorToWorkerTaskSpawner<'a> {
370460
let request = Request::from_parts(
371461
MetadataMap::from_headers(headers),
372462
Extensions::default(),
373-
futures::stream::once(async { msg }).chain(UnboundedReceiverStream::new(rx)),
463+
futures::stream::once(async { msg })
464+
.chain(UnboundedReceiverStream::new(coordinator_to_worker_rx)),
374465
);
375466

376467
let metrics = self.metrics.clone();
468+
377469
self.join_set.spawn(async move {
378470
let start = Instant::now();
379471
let mut client = channel_resolver.get_worker_client_for_url(&url).await?;
380-
client.coordinator_channel(request).await.map_err(|e| {
472+
let response = client.coordinator_channel(request).await.map_err(|e| {
381473
tonic_status_to_datafusion_error(&e).unwrap_or_else(|| {
382474
exec_datafusion_err!("Error sending plan to worker {url}: {e}")
383475
})
384476
})?;
385477
metrics.plan_send_latency.record(&start);
386478
metrics.plan_bytes_sent.add(plan_size);
479+
let mut stream = response.into_inner();
480+
while let Some(msg) = stream.next().await {
481+
if worker_to_coordinator_tx.send(msg).is_err() {
482+
break; // receiver dropped
483+
}
484+
}
387485
Ok::<_, DataFusionError>(())
388486
});
389-
Ok(tx)
487+
488+
Ok((coordinator_to_worker_tx, worker_to_coordinator_rx))
489+
}
490+
491+
/// Receives worker-to-coordinator messages and inserts any collected metrics into the store.
492+
/// Runs in a detached spawn so it is not cancelled when the output stream is dropped early.
493+
fn metrics_collection_task(
494+
&mut self,
495+
task_i: usize,
496+
mut worker_to_coordinator_rx: WorkerResponseRx,
497+
) {
498+
let task_key = TaskKey {
499+
query_id: serialize_uuid(&self.query_id),
500+
stage_id: self.stage_id as u64,
501+
task_number: task_i as u64,
502+
};
503+
let task_metrics_collection = Arc::clone(self.task_metrics);
504+
tokio::spawn(async move {
505+
while let Some(Ok(msg)) = worker_to_coordinator_rx.recv().await {
506+
let Some(worker_to_coordinator_msg::Inner::TaskMetrics(pre_order_metrics)) =
507+
msg.inner
508+
else {
509+
continue;
510+
};
511+
task_metrics_collection.insert(task_key.clone(), pre_order_metrics.metrics);
512+
}
513+
});
390514
}
391515

392516
/// Instantiates and returns the task that based on the different local [WorkUnitFeedExec]

src/execution_plans/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub mod benchmarks;
1313

1414
pub use broadcast::BroadcastExec;
1515
pub use children_isolator_union::ChildrenIsolatorUnionExec;
16-
pub use distributed::DistributedExec;
16+
pub use distributed::{DistributedExec, MetricsStore};
1717
pub(crate) use metrics::MetricsWrapperExec;
1818
pub use network_broadcast::NetworkBroadcastExec;
1919
pub use network_coalesce::NetworkCoalesceExec;

src/execution_plans/network_broadcast.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ use crate::common::require_one_child;
22
use crate::distributed_planner::NetworkBoundary;
33
use crate::stage::{LocalStage, Stage};
44
use crate::worker::WorkerConnectionPool;
5-
use crate::worker::generated::worker as pb;
6-
use crate::worker::generated::worker::TaskKey;
7-
use crate::worker::generated::worker::flight_app_metadata;
85
use crate::{BroadcastExec, DistributedTaskContext};
9-
use dashmap::DashMap;
106
use datafusion::common::tree_node::Transformed;
117
use datafusion::common::{Result, not_impl_err, plan_err};
128
use datafusion::error::DataFusionError;
@@ -125,7 +121,6 @@ pub struct NetworkBroadcastExec {
125121
pub(crate) properties: Arc<PlanProperties>,
126122
pub(crate) input_stage: Stage,
127123
pub(crate) worker_connections: WorkerConnectionPool,
128-
pub(crate) metrics_collection: Arc<DashMap<TaskKey, Vec<pb::MetricsSet>>>,
129124
}
130125

131126
impl NetworkBroadcastExec {
@@ -155,7 +150,6 @@ impl NetworkBroadcastExec {
155150
properties,
156151
worker_connections: WorkerConnectionPool::new(0),
157152
input_stage: Stage::Local(input_stage),
158-
metrics_collection: Default::default(),
159153
}
160154
}
161155

@@ -265,16 +259,7 @@ impl ExecutionPlan for NetworkBroadcastExec {
265259
&context,
266260
)?;
267261

268-
let metrics_collection = Arc::clone(&self.metrics_collection);
269-
let stream = worker_connection.stream_partition(off + partition, move |meta| {
270-
if let Some(flight_app_metadata::Content::MetricsCollection(m)) = meta.content {
271-
for task_metrics in m.tasks {
272-
if let Some(task_key) = task_metrics.task_key {
273-
metrics_collection.insert(task_key, task_metrics.metrics);
274-
};
275-
}
276-
}
277-
})?;
262+
let stream = worker_connection.stream_partition(off + partition, |_meta| {})?;
278263
streams.push(stream);
279264
}
280265

0 commit comments

Comments
 (0)