@@ -6,17 +6,20 @@ use crate::networking::get_distributed_worker_resolver;
66use crate :: passthrough_headers:: get_passthrough_headers;
77use crate :: protobuf:: { DistributedCodec , tonic_status_to_datafusion_error} ;
88use crate :: stage:: { LocalStage , RemoteStage , Stage } ;
9+ use crate :: worker:: generated:: worker as pb;
910use crate :: worker:: generated:: worker:: set_plan_request:: WorkUnitFeedDeclaration ;
1011use 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} ;
1315use crate :: {
1416 DISTRIBUTED_DATAFUSION_TASK_ID_LABEL , DistributedConfig , DistributedTaskContext ,
1517 DistributedWorkUnitFeedContext , WorkerResolver , get_distributed_channel_resolver,
1618} ;
19+ use datafusion:: common:: HashMap ;
1720use datafusion:: common:: instant:: Instant ;
1821use datafusion:: common:: runtime:: JoinSet ;
19- use datafusion:: common:: tree_node:: { Transformed , TreeNode } ;
22+ use datafusion:: common:: tree_node:: { Transformed , TreeNode , TreeNodeRecursion } ;
2023use datafusion:: common:: { Result , exec_err} ;
2124use datafusion:: common:: { exec_datafusion_err, internal_datafusion_err} ;
2225use datafusion:: error:: DataFusionError ;
@@ -41,12 +44,46 @@ use std::sync::Mutex;
4144use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
4245use std:: time:: Duration ;
4346use tokio:: sync:: mpsc:: UnboundedSender ;
47+ use tokio:: sync:: watch;
4448use tokio_stream:: wrappers:: UnboundedReceiverStream ;
4549use tonic:: Request ;
4650use tonic:: metadata:: MetadataMap ;
4751use url:: Url ;
4852use 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
63101struct 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+
257339struct 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]
0 commit comments