Skip to main content

datafusion_distributed/protocol/grpc/
worker_client.rs

1use super::channel_resolver::BoxCloneSyncChannel;
2use super::errors::{map_flight_to_datafusion_error, map_status_to_datafusion_error};
3use super::generated::worker as pb;
4use super::metrics_proto::metrics_set_proto_to_df;
5use crate::common::serialize_uuid;
6use crate::grpc::generated::worker::FlightAppMetadata;
7use crate::grpc::on_drop_stream::on_drop_stream;
8use crate::{
9    BytesMetricExt, CoordinatorToWorkerMsg, DISTRIBUTED_DATAFUSION_TASK_ID_LABEL,
10    DistributedConfig, ExecuteTaskRequest, FirstLatencyMetric, GetWorkerInfoRequest,
11    GetWorkerInfoResponse, LatencyMetricExt, LoadInfo, MaxLatencyMetric, MaybeEncoded,
12    MinLatencyMetric, P50LatencyMetric, P95LatencyMetric, ProducerHead, SetPlanRequest, TaskKey,
13    TaskMetrics, WorkUnitBatch, WorkUnitFeedDeclaration, WorkUnitMsg, WorkerChannel,
14    WorkerToCoordinatorMsg,
15};
16use arrow_flight::FlightData;
17use arrow_flight::decode::FlightRecordBatchStream;
18use arrow_flight::error::FlightError;
19use async_trait::async_trait;
20use datafusion::arrow::array::RecordBatch;
21use datafusion::common::instant::Instant;
22use datafusion::common::runtime::SpawnedTask;
23use datafusion::common::{DataFusionError, Result};
24use datafusion::execution::TaskContext;
25use datafusion::execution::memory_pool::MemoryConsumer;
26use datafusion::physical_expr_common::metrics::{Count, Label, MetricBuilder, MetricValue, Time};
27use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
28use futures::stream::BoxStream;
29use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
30use http::{Extensions, HeaderMap};
31use pin_project::{pin_project, pinned_drop};
32use prost::Message;
33use std::borrow::Cow;
34use std::pin::Pin;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicUsize, Ordering};
37use std::task::{Context, Poll};
38use std::time::{Duration, SystemTime, UNIX_EPOCH};
39use tokio::sync::Notify;
40use tokio::sync::mpsc::UnboundedSender;
41use tokio_stream::wrappers::UnboundedReceiverStream;
42use tokio_util::sync::CancellationToken;
43use tonic::metadata::MetadataMap;
44use tonic::{Request, Status};
45
46#[async_trait]
47impl WorkerChannel for pb::worker_service_client::WorkerServiceClient<BoxCloneSyncChannel> {
48    async fn coordinator_channel(
49        &mut self,
50        headers: HeaderMap,
51        set_plan_request: SetPlanRequest,
52        c2w_stream: BoxStream<'static, CoordinatorToWorkerMsg>,
53        metrics: ExecutionPlanMetricsSet,
54        ctx: &Arc<TaskContext>,
55    ) -> Result<BoxStream<'static, Result<WorkerToCoordinatorMsg>>> {
56        let set_plan_request = encode_set_plan_request(set_plan_request, ctx)?;
57        let plan_bytes_sent = set_plan_request.plan_proto.len();
58        let input_stream = futures::stream::once(async move {
59            pb::CoordinatorToWorkerMsg {
60                inner: Some(pb::coordinator_to_worker_msg::Inner::SetPlanRequest(
61                    set_plan_request,
62                )),
63            }
64        })
65        .chain(c2w_stream.map(encode_coordinator_to_worker_msg));
66
67        let output_stream = self
68            .coordinator_channel(Request::from_parts(
69                MetadataMap::from_headers(headers),
70                Extensions::default(),
71                input_stream,
72            ))
73            .boxed()
74            .await
75            .map_err(map_status_to_datafusion_error)?
76            .into_inner()
77            .map_err(map_status_to_datafusion_error)
78            .map(|msg| decode_worker_to_coordinator_msg(msg?))
79            .boxed();
80
81        MetricBuilder::new(&metrics)
82            .with_label(Label::new(DISTRIBUTED_DATAFUSION_TASK_ID_LABEL, "0"))
83            .bytes_counter("plan_bytes_sent")
84            .add_bytes(plan_bytes_sent);
85
86        Ok(output_stream)
87    }
88
89    async fn execute_task(
90        &mut self,
91        headers: HeaderMap,
92        request: ExecuteTaskRequest,
93        metrics: ExecutionPlanMetricsSet,
94        ctx: &Arc<TaskContext>,
95    ) -> Result<Vec<BoxStream<'static, Result<RecordBatch>>>> {
96        let d_cfg = DistributedConfig::from_session_config(ctx.session_config())?;
97        let buffer_budget_bytes = d_cfg.worker_connection_buffer_budget_bytes;
98
99        // We are retaining record batches in memory until they are consumed, so we need to account
100        // for them in the memory pool.
101        let memory_reservation =
102            Arc::new(MemoryConsumer::new("WorkerConnection").register(ctx.memory_pool()));
103        let memory_reservation_clone = Arc::clone(&memory_reservation);
104
105        // Track the maximum memory used to buffer recieved messages.
106        let mut curr_max_mem = 0;
107        let max_mem_used = MetricBuilder::new(&metrics).global_gauge("max_mem_used");
108        // Track the total encoded size of all recieved messages.
109        let bytes_transferred = MetricBuilder::new(&metrics).bytes_counter("bytes_transferred");
110        let msg_count = MetricBuilder::new(&metrics).global_counter("msg_count");
111        // Track end-to-end network latency distribution for messages that actually arrive.
112        let mut latency_metrics = NetworkLatencyMetrics::new(&metrics);
113        // Track the total CPU time spent in polling messages over the network + decoding them.
114        let elapsed_compute = Time::new();
115        let elapsed_compute_clone = elapsed_compute.clone();
116        MetricBuilder::new(&metrics).build(MetricValue::ElapsedCompute(elapsed_compute.clone()));
117
118        let target_partition_range = request.target_partition_start..request.target_partition_end;
119        let request = pb::ExecuteTaskRequest {
120            task_key: Some(encode_task_key(request.task_key)),
121            target_partition_start: request.target_partition_start as u64,
122            target_partition_end: request.target_partition_end as u64,
123            producer_head: Some(encode_producer_head(request.producer_head, ctx)?),
124        };
125        let metadata = MetadataMap::from_headers(headers);
126
127        // The senders and receivers are unbounded queues used for multiplexing the record
128        // batches sent through the single gRPC stream into one stream per partition. They
129        // are unbounded to avoid head-of-line blocking: a single bounded queue could block
130        // the demux task and starve all sibling partitions even though they have capacity,
131        // which deadlocks queries with cross-partition dependencies.
132        // Total memory is bounded globally below via `mem_available_notify`.
133        let mut per_partition_tx = Vec::with_capacity(target_partition_range.len());
134        let mut per_partition_rx = Vec::with_capacity(target_partition_range.len());
135        for _partition in target_partition_range.clone() {
136            let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<WorkerMsg>();
137            per_partition_tx.push(tx);
138            per_partition_rx.push(rx);
139        }
140
141        let mem_available_notify = Arc::new(Notify::new());
142        let mem_available_notify_for_task = Arc::clone(&mem_available_notify);
143
144        let first_poll_notify = Arc::new(Notify::new());
145        let first_poll_notify_for_task = Arc::clone(&first_poll_notify);
146
147        // Cancellation token allows us to stop the background task promptly when all partition
148        // streams are dropped (e.g., when the query is cancelled).
149        let cancel_token = CancellationToken::new();
150        let cancel = cancel_token.clone();
151
152        let mut self_clone = self.clone();
153        let request_for_task = request.clone();
154        let metadata_for_task = metadata.clone();
155
156        // This task will pull data from all the partitions in `target_partition_range`, and will
157        // fan them out to the appropriate `per_partition_rx` based on the "partition" declared
158        // in each individual record batch flight metadata.
159        let task = SpawnedTask::spawn(async move {
160            tokio::select! {
161                biased;
162                _ = cancel.cancelled() => {
163                    // If all SendableRecordBatchStreams canceled before any poll, we need to
164                    // anyway trigger the task execution and cancel it immediately so that the
165                    // cancellation is propagated also in the remote worker. Otherwise, it might
166                    // hang forever waiting for someone to execute it.
167                    let _ = self_clone.execute_task(Request::from_parts(
168                        metadata_for_task,
169                        Extensions::default(),
170                        request_for_task,
171                    )).await;
172                    return
173                },
174                _ = first_poll_notify_for_task.notified() => {}
175            }
176
177            let request = Request::from_parts(
178                metadata_for_task,
179                Extensions::default(),
180                request_for_task,
181            );
182            let mut interleaved_stream = match self_clone.execute_task(request).await {
183                Ok(v) => v.into_inner(),
184                Err(err) => return fanout(&per_partition_tx, err),
185            };
186
187            loop {
188                // Backpressure gate. Per-partition channels are unbounded, so we cap
189                // total in-flight buffered bytes here by pausing the gRPC pull when
190                // consumers haven't drained enough. This propagates flow control all
191                // the way back to the worker without coupling sibling partitions.
192                // We always allow a message through when reservation == 0 to avoid
193                // livelock if a single message is larger than the budget.
194                while memory_reservation.size() >= buffer_budget_bytes {
195                    tokio::select! {
196                        biased;
197                        _ = cancel.cancelled() => return,
198                        _ = mem_available_notify_for_task.notified() => {}
199                    }
200                }
201
202                // Check for cancellation while waiting for the next message.
203                let flight_data = tokio::select! {
204                    biased;
205                    _ = cancel.cancelled() => return,
206                    msg = interleaved_stream.next() => {
207                        match msg {
208                            Some(Ok(v)) => v,
209                            Some(Err(err)) => return fanout(&per_partition_tx, err),
210                            None => return, // Stream exhausted
211                        }
212                    }
213                };
214
215                // Earliest time at which the msg was received.
216                let msg_received_time = SystemTime::now();
217
218                let flight_metadata = match FlightAppMetadata::decode(flight_data.app_metadata.as_ref()) {
219                    Ok(v) => v,
220                    Err(err) => {
221                        return fanout(&per_partition_tx, Status::internal(err.to_string()));
222                    }
223                };
224
225                // Update the running latency tracker.
226                let sent_time = UNIX_EPOCH + Duration::from_nanos(flight_metadata.created_timestamp_unix_nanos);
227                if flight_metadata.created_timestamp_unix_nanos > 0
228                    && let Ok(delta) = msg_received_time.duration_since(sent_time) {
229                    latency_metrics.add_duration(delta);
230                }
231
232                let partition = flight_metadata.partition as usize;
233                // the `per_partition_tx` variable is using a normal `Vec` for storing the
234                // channel transmitters, so we need to subtract the `target_partition_range.start`
235                // to the `partition` in order to offset it to the appropriate index.
236                let Some(sender_i) = partition.checked_sub(target_partition_range.start) else {
237                    let msg = format!(
238                        "Received partition {partition} in Flight metadata, but available partitions are {target_partition_range:?}"
239                    );
240                    return fanout(&per_partition_tx, Status::internal(msg));
241                };
242
243                let Some(o_tx) = per_partition_tx.get(sender_i) else {
244                    let msg = format!(
245                        "Received partition {partition} in Flight metadata, but available partitions are {target_partition_range:?}"
246                    );
247                    return fanout(&per_partition_tx, Status::internal(msg));
248                };
249
250                // We need to send the memory reservation in the same tuple as the actual message
251                // so that it gets dropped as soon as the message leaves the queue. Dropping the
252                // memory reservation means releasing the memory from the pool for that specific
253                // message
254                let size = flight_data.encoded_len();
255                memory_reservation.grow(size);
256
257                // Update memory related metrics.
258                msg_count.add(1);
259                bytes_transferred.add_bytes(size);
260                let curr_mem = memory_reservation.size();
261                if curr_mem > curr_max_mem {
262                    curr_max_mem = curr_mem;
263                    max_mem_used.set(curr_max_mem);
264                }
265
266                if o_tx.send(Ok((flight_data, flight_metadata))).is_err() {
267                    // The receiver for this partition was dropped (e.g. a hash join partition
268                    // completed early without consuming its probe side). Don't exit: other
269                    // partitions multiplexed over the same gRPC stream still need their data.
270                    // Undo the memory reservation that was grown for this dropped batch.
271                    memory_reservation.shrink(size);
272                    continue;
273                };
274            }
275        }.with_elapsed_compute(elapsed_compute));
276
277        let task = Arc::new(task);
278        let not_consumed_streams = Arc::new(AtomicUsize::new(per_partition_rx.len()));
279
280        let mut result = Vec::with_capacity(per_partition_rx.len());
281        for partition_receiver in per_partition_rx {
282            let task = Arc::clone(&task);
283            let cancel_token = cancel_token.clone();
284
285            let first_poll_notify = Arc::clone(&first_poll_notify);
286            let stream = async move {
287                first_poll_notify.notify_one();
288                UnboundedReceiverStream::new(partition_receiver)
289            }
290            .flatten_stream();
291
292            let stream = stream.map_err(|err| FlightError::Tonic(Box::new(err)));
293            let reservation = Arc::clone(&memory_reservation_clone);
294            let mem_available_notify = Arc::clone(&mem_available_notify);
295            let stream = stream.map_ok(move |(data, _meta)| {
296                reservation.shrink(data.encoded_len());
297                // Wake the demux task in case it is blocked on the byte budget.
298                mem_available_notify.notify_one();
299                let _ = &task; // <- keep the task that polls data from the network alive.
300                data
301            });
302            let stream = FlightRecordBatchStream::new_from_flight_data(stream);
303            let stream = stream.map_err(map_flight_to_datafusion_error);
304            let stream = stream.with_elapsed_compute(elapsed_compute_clone.clone());
305
306            // When the stream is dropped, cancel the background task to ensure prompt cleanup.
307            let not_consumed_streams = Arc::clone(&not_consumed_streams);
308            result.push(
309                on_drop_stream(stream, move || {
310                    let remaining_streams = not_consumed_streams.fetch_sub(1, Ordering::SeqCst) - 1;
311                    if remaining_streams == 0 {
312                        cancel_token.cancel();
313                    }
314                })
315                .boxed(),
316            );
317        }
318
319        Ok(result)
320    }
321
322    async fn get_worker_info(
323        &mut self,
324        _request: GetWorkerInfoRequest,
325    ) -> Result<GetWorkerInfoResponse> {
326        let response = self
327            .get_worker_info(pb::GetWorkerInfoRequest {})
328            .await
329            .map_err(map_status_to_datafusion_error)?
330            .into_inner();
331        Ok(GetWorkerInfoResponse {
332            version: response.version,
333        })
334    }
335}
336
337type WorkerMsg = Result<(FlightData, FlightAppMetadata), Status>;
338
339struct NetworkLatencyMetrics {
340    metrics: ExecutionPlanMetricsSet,
341    values: Option<NetworkLatencyMetricValues>,
342}
343
344impl NetworkLatencyMetrics {
345    fn new(metrics: &ExecutionPlanMetricsSet) -> Self {
346        Self {
347            metrics: metrics.clone(),
348            values: None,
349        }
350    }
351
352    fn add_duration(&mut self, duration: Duration) {
353        self.values
354            .get_or_insert_with(|| NetworkLatencyMetricValues::new(&self.metrics))
355            .add_duration(duration);
356    }
357}
358
359struct NetworkLatencyMetricValues {
360    min_latency: MinLatencyMetric,
361    max_latency: MaxLatencyMetric,
362    p50_latency: P50LatencyMetric,
363    p95_latency: P95LatencyMetric,
364    first_latency: FirstLatencyMetric,
365    sum_latency: Time,
366    latency_count: Count,
367}
368
369impl NetworkLatencyMetricValues {
370    fn new(metrics: &ExecutionPlanMetricsSet) -> Self {
371        let min_latency = MetricBuilder::new(metrics).min_latency("network_latency_min");
372        let max_latency = MetricBuilder::new(metrics).max_latency("network_latency_max");
373        let p50_latency = MetricBuilder::new(metrics).p50_latency("network_latency_p50");
374        let p95_latency = MetricBuilder::new(metrics).p95_latency("network_latency_p95");
375        let first_latency = MetricBuilder::new(metrics).first_latency("network_latency_first");
376        let sum_latency = Time::new();
377        MetricBuilder::new(metrics).build(MetricValue::Time {
378            name: Cow::Borrowed("network_latency_sum"),
379            time: sum_latency.clone(),
380        });
381        let latency_count = MetricBuilder::new(metrics).counter("network_latency_count", 0);
382
383        Self {
384            min_latency,
385            max_latency,
386            p50_latency,
387            p95_latency,
388            first_latency,
389            sum_latency,
390            latency_count,
391        }
392    }
393
394    fn add_duration(&self, duration: Duration) {
395        self.min_latency.add_duration(duration);
396        self.max_latency.add_duration(duration);
397        self.p50_latency.add_duration(duration);
398        self.p95_latency.add_duration(duration);
399        self.first_latency.add_duration(duration);
400        self.sum_latency.add_duration(duration);
401        self.latency_count.add(1);
402    }
403}
404
405pub(super) fn encode_producer_head(
406    head: ProducerHead,
407    ctx: &Arc<TaskContext>,
408) -> Result<pb::execute_task_request::ProducerHead> {
409    Ok(match head {
410        ProducerHead::None => pb::execute_task_request::ProducerHead::None(pb::NoneHead {}),
411        ProducerHead::BroadcastExec { output_partitions } => {
412            pb::execute_task_request::ProducerHead::Broadcast(pb::BroadcastExecHead {
413                output_partitions: output_partitions as u64,
414            })
415        }
416        ProducerHead::RepartitionExec { partitioning } => {
417            pb::execute_task_request::ProducerHead::Repartition(pb::RepartitionExecHead {
418                partitioning: partitioning.encode(ctx)?,
419            })
420        }
421    })
422}
423
424fn encode_coordinator_to_worker_msg(msg: CoordinatorToWorkerMsg) -> pb::CoordinatorToWorkerMsg {
425    pb::CoordinatorToWorkerMsg {
426        inner: Some(match msg {
427            CoordinatorToWorkerMsg::WorkUnitBatch(batch) => {
428                pb::coordinator_to_worker_msg::Inner::WorkUnitBatch(encode_work_unit_batch(batch))
429            }
430            CoordinatorToWorkerMsg::WorkUnitEos => {
431                pb::coordinator_to_worker_msg::Inner::WorkUnitEos(true)
432            }
433        }),
434    }
435}
436
437fn encode_set_plan_request(
438    request: SetPlanRequest,
439    ctx: &Arc<TaskContext>,
440) -> Result<pb::SetPlanRequest> {
441    let plan_proto = request.plan.encode(ctx)?;
442    Ok(pb::SetPlanRequest {
443        task_key: Some(encode_task_key(request.task_key)),
444        task_count: request.task_count as u64,
445        plan_proto,
446        work_unit_feed_declarations: request
447            .work_unit_feed_declarations
448            .into_iter()
449            .map(encode_work_unit_feed_declaration)
450            .collect(),
451        target_worker_url: request.target_worker_url.to_string(),
452        query_start_time_ns: request.query_start_time_ns as u64,
453    })
454}
455
456fn encode_work_unit_batch(batch: WorkUnitBatch) -> pb::WorkUnitBatch {
457    pb::WorkUnitBatch {
458        batch: batch.batch.into_iter().map(encode_work_unit).collect(),
459    }
460}
461
462fn encode_work_unit(work_unit: WorkUnitMsg) -> pb::WorkUnit {
463    pb::WorkUnit {
464        id: serialize_uuid(&work_unit.id),
465        partition: work_unit.partition as u64,
466        body: match work_unit.body {
467            MaybeEncoded::Encoded(body) => body,
468            MaybeEncoded::Decoded(body) => body.encode_to_bytes(),
469        },
470        created_timestamp_unix_nanos: work_unit.created_timestamp_unix_nanos as u64,
471        sent_timestamp_unix_nanos: work_unit.sent_timestamp_unix_nanos as u64,
472        received_timestamp_unix_nanos: work_unit.received_timestamp_unix_nanos as u64,
473        processed_timestamp_unix_nanos: work_unit.processed_timestamp_unix_nanos as u64,
474    }
475}
476
477fn encode_work_unit_feed_declaration(
478    declaration: WorkUnitFeedDeclaration,
479) -> pb::set_plan_request::WorkUnitFeedDeclaration {
480    pb::set_plan_request::WorkUnitFeedDeclaration {
481        id: serialize_uuid(&declaration.id),
482        partitions: declaration.partitions as u64,
483    }
484}
485
486fn encode_task_key(task_key: TaskKey) -> pb::TaskKey {
487    pb::TaskKey {
488        query_id: serialize_uuid(&task_key.query_id),
489        stage_id: task_key.stage_id as u64,
490        task_number: task_key.task_number as u64,
491    }
492}
493
494fn decode_worker_to_coordinator_msg(
495    msg: pb::WorkerToCoordinatorMsg,
496) -> Result<WorkerToCoordinatorMsg> {
497    Ok(
498        match msg
499            .inner
500            .ok_or_else(|| missing("WorkerToCoordinatorMsg.inner"))?
501        {
502            pb::worker_to_coordinator_msg::Inner::TaskMetrics(task_metrics) => {
503                WorkerToCoordinatorMsg::TaskMetrics(decode_task_metrics(task_metrics)?)
504            }
505            pb::worker_to_coordinator_msg::Inner::LoadInfo(load_info) => {
506                WorkerToCoordinatorMsg::LoadInfo(decode_load_info(load_info))
507            }
508            pb::worker_to_coordinator_msg::Inner::LoadInfoEos(_) => {
509                WorkerToCoordinatorMsg::LoadInfoEos
510            }
511        },
512    )
513}
514
515fn decode_task_metrics(task_metrics: pb::TaskMetrics) -> Result<TaskMetrics> {
516    Ok(TaskMetrics {
517        pre_order_plan_metrics: task_metrics
518            .pre_order_plan_metrics
519            .into_iter()
520            .map(|metrics_set| metrics_set_proto_to_df(&metrics_set))
521            .collect::<Result<_>>()?,
522        task_metrics: metrics_set_proto_to_df(
523            &task_metrics
524                .task_metrics
525                .ok_or_else(|| missing("task_metrics"))?,
526        )?,
527    })
528}
529
530fn decode_load_info(load_info: pb::LoadInfo) -> LoadInfo {
531    LoadInfo {
532        partition: load_info.partition as usize,
533        rows_ready: load_info.rows_ready as usize,
534        per_column_bytes_ready: load_info
535            .per_column_bytes_ready
536            .into_iter()
537            .map(|bytes| bytes as usize)
538            .collect(),
539        per_column_ndv_percentage: load_info.per_column_ndv_percentage,
540        per_column_null_percentage: load_info.per_column_null_percentage,
541        rows_pulled_from_leaf: load_info.rows_pulled_from_leaf as usize,
542        reached_eos: load_info.reached_eos,
543    }
544}
545
546fn missing(field: &'static str) -> DataFusionError {
547    DataFusionError::Internal(format!("Missing field '{field}'"))
548}
549
550fn fanout(o_txs: &[UnboundedSender<WorkerMsg>], err: Status) {
551    for o_tx in o_txs {
552        let _ = o_tx.send(Err(err.clone()));
553    }
554}
555
556/// Creates a [`WorkerServiceClient`] with high default message size limits.
557///
558/// This is a convenience function that wraps [`WorkerServiceClient::new`] and configures
559/// it with `max_decoding_message_size(usize::MAX)` and `max_encoding_message_size(usize::MAX)`
560/// to avoid message size limitations for internal communication.
561///
562/// Users implementing custom [`ChannelResolver`]s should use this function in their
563/// `get_worker_client_for_url` implementations to ensure consistent behavior with built-in
564/// implementations.
565///
566/// # Example
567///
568/// ```rust
569/// # use datafusion::common::DataFusionError;
570/// # use datafusion::error::Result;
571/// # use tonic::transport::Channel;
572/// # use url::Url;
573/// # use datafusion_distributed::{ChannelResolver, WorkerChannel, grpc};
574///
575/// struct MyResolver;
576///
577/// #[async_trait::async_trait]
578/// impl ChannelResolver for MyResolver {
579///     async fn get_worker_client_for_url(&self, url: &Url) -> Result<Box<dyn WorkerChannel>> {
580///         let channel = Channel::from_shared(url.to_string())
581///             .map_err(|err| DataFusionError::External(Box::new(err)))?
582///             .connect()
583///             .await
584///             .map_err(|err| DataFusionError::External(Box::new(err)))?;
585///         Ok(grpc::create_worker_client(grpc::BoxCloneSyncChannel::new(channel)))
586///     }
587/// }
588/// ```
589pub fn create_worker_client(channel: BoxCloneSyncChannel) -> Box<dyn WorkerChannel> {
590    Box::new(
591        pb::worker_service_client::WorkerServiceClient::new(channel)
592            .max_decoding_message_size(usize::MAX)
593            .max_encoding_message_size(usize::MAX),
594    )
595}
596
597trait ElapsedComputeFutureExt: Future + Sized {
598    fn with_elapsed_compute(self, elapsed_compute: Time) -> ElapsedComputeFuture<Self>;
599}
600
601trait ElapsedComputeStreamExt: Stream + Sized {
602    fn with_elapsed_compute(self, elapsed_compute: Time) -> ElapsedComputeStream<Self>;
603}
604
605impl<O, F: Future<Output = O>> ElapsedComputeFutureExt for F {
606    fn with_elapsed_compute(self, elapsed_compute: Time) -> ElapsedComputeFuture<Self> {
607        ElapsedComputeFuture {
608            inner: self,
609            curr: Duration::default(),
610            elapsed_compute,
611        }
612    }
613}
614
615impl<O, S: Stream<Item = O>> ElapsedComputeStreamExt for S {
616    fn with_elapsed_compute(self, elapsed_compute: Time) -> ElapsedComputeStream<Self> {
617        ElapsedComputeStream {
618            inner: self,
619            curr: Duration::default(),
620            elapsed_compute,
621        }
622    }
623}
624
625#[pin_project(PinnedDrop)]
626struct ElapsedComputeStream<T> {
627    #[pin]
628    inner: T,
629    curr: Duration,
630    elapsed_compute: Time,
631}
632
633/// Drop implementation that ensures that any accumulated time is properly dumped to the metric
634/// in case the stream gets dropped before completion.
635#[pinned_drop]
636impl<T> PinnedDrop for ElapsedComputeStream<T> {
637    fn drop(self: Pin<&mut Self>) {
638        if self.curr > Duration::default() {
639            let self_projected = self.project();
640            self_projected
641                .elapsed_compute
642                .add_duration(*self_projected.curr);
643        }
644    }
645}
646
647impl<O, F: Stream<Item = O>> Stream for ElapsedComputeStream<F> {
648    type Item = O;
649
650    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
651        let self_projected = self.project();
652        let start = Instant::now();
653        let result = self_projected.inner.poll_next(cx);
654        *self_projected.curr += start.elapsed();
655        if result.is_ready() {
656            self_projected
657                .elapsed_compute
658                .add_duration(*self_projected.curr);
659            *self_projected.curr = Duration::default();
660        }
661        result
662    }
663}
664
665#[pin_project(PinnedDrop)]
666struct ElapsedComputeFuture<T> {
667    #[pin]
668    inner: T,
669    curr: Duration,
670    elapsed_compute: Time,
671}
672
673/// Drop implementation that ensures that any accumulated time is properly dumped to the metric
674/// in case the future gets dropped before completion.
675#[pinned_drop]
676impl<T> PinnedDrop for ElapsedComputeFuture<T> {
677    fn drop(self: Pin<&mut Self>) {
678        if self.curr > Duration::default() {
679            let self_projected = self.project();
680            self_projected
681                .elapsed_compute
682                .add_duration(*self_projected.curr);
683        }
684    }
685}
686
687impl<O, F: Future<Output = O>> Future for ElapsedComputeFuture<F> {
688    type Output = O;
689
690    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
691        let self_projected = self.project();
692        let start = Instant::now();
693        let result = self_projected.inner.poll(cx);
694        *self_projected.curr += start.elapsed();
695        if result.is_ready() {
696            self_projected
697                .elapsed_compute
698                .add_duration(*self_projected.curr);
699            *self_projected.curr = Duration::default();
700        }
701        result
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use futures::StreamExt;
709    use futures::stream::unfold;
710
711    #[tokio::test]
712    async fn elapsed_compute_future() {
713        async fn cheap() {
714            tokio::time::sleep(Duration::from_millis(1)).await;
715        }
716
717        async fn expensive() {
718            let mut _count = 0f64;
719            for i in 0..100000 {
720                tokio::task::yield_now().await;
721                _count /= i as f64
722            }
723        }
724
725        let cheap_time = Time::new();
726        cheap().with_elapsed_compute(cheap_time.clone()).await;
727        println!("cheap future: {}", cheap_time.value());
728
729        let expensive_time = Time::new();
730        expensive()
731            .with_elapsed_compute(expensive_time.clone())
732            .await;
733        println!("expensive future: {}", expensive_time.value());
734
735        assert!(expensive_time.value() > cheap_time.value());
736    }
737
738    #[tokio::test]
739    async fn elapsed_compute_stream() {
740        fn cheap() -> impl Stream<Item = i64> {
741            unfold(0i64, |state| async move {
742                if state < 10 {
743                    tokio::time::sleep(Duration::from_micros(10)).await;
744                    Some((state, state + 1))
745                } else {
746                    None
747                }
748            })
749        }
750
751        fn expensive() -> impl Stream<Item = i64> {
752            unfold(0i64, |state| async move {
753                if state < 10 {
754                    // Simulate expensive computation
755                    let mut _count = 0f64;
756                    for i in 1..100000 {
757                        _count += (i as f64).sqrt();
758                    }
759                    tokio::task::yield_now().await;
760                    Some((state, state + 1))
761                } else {
762                    None
763                }
764            })
765        }
766
767        let cheap_time = Time::new();
768        cheap()
769            .with_elapsed_compute(cheap_time.clone())
770            .collect::<Vec<_>>()
771            .await;
772        println!("cheap future: {}", cheap_time.value());
773
774        let expensive_time = Time::new();
775        expensive()
776            .with_elapsed_compute(expensive_time.clone())
777            .collect::<Vec<_>>()
778            .await;
779        println!("expensive future: {}", expensive_time.value());
780
781        assert!(expensive_time.value() > cheap_time.value());
782    }
783}