datafusion_distributed/protocol/grpc/
worker_service.rs1use super::errors::{datafusion_error_to_tonic_status, map_status_to_datafusion_error};
2use super::generated::worker as pb;
3use super::metrics_proto::df_metrics_set_to_proto;
4use super::spawn_select_all::spawn_select_all;
5
6use crate::common::{deserialize_uuid, now_ns};
7use crate::protocol::grpc::{ObservabilityServiceImpl, ObservabilityServiceServer};
8use crate::{
9 CoordinatorToWorkerMsg, DistributedConfig, ExecuteTaskRequest, LoadInfo, MaybeEncoded,
10 ProducerHead, SetPlanRequest, TaskKey, TaskMetrics, WorkUnitBatch, WorkUnitFeedDeclaration,
11 WorkUnitMsg, Worker, WorkerResolver, WorkerToCoordinatorMsg,
12};
13
14use arrow_flight::FlightData;
15use arrow_flight::encode::{DictionaryHandling, FlightDataEncoder, FlightDataEncoderBuilder};
16use arrow_flight::error::FlightError;
17use arrow_select::dictionary::garbage_collect_any_dictionary;
18use async_trait::async_trait;
19use datafusion::arrow::array::{Array, AsArray, RecordBatch, RecordBatchOptions};
20use datafusion::arrow::ipc::CompressionType;
21use datafusion::arrow::ipc::writer::IpcWriteOptions;
22use datafusion::common::DataFusionError;
23use datafusion::execution::SendableRecordBatchStream;
24use futures::stream::BoxStream;
25use futures::{StreamExt, TryStreamExt};
26use prost::Message;
27use std::sync::Arc;
28use tonic::{Request, Response, Status, Streaming};
29use url::Url;
30
31const RECORD_BATCH_BUFFER_SIZE: usize = 2;
32
33impl Worker {
34 pub fn into_worker_server(self) -> pb::worker_service_server::WorkerServiceServer<Self> {
62 pb::worker_service_server::WorkerServiceServer::new(self)
63 .max_decoding_message_size(usize::MAX)
64 .max_encoding_message_size(usize::MAX)
65 }
66
67 pub fn with_observability_service(
73 &self,
74 worker_resolver: Arc<dyn WorkerResolver + Send + Sync>,
75 ) -> ObservabilityServiceServer<ObservabilityServiceImpl> {
76 ObservabilityServiceServer::new(ObservabilityServiceImpl::new(
77 self.task_data_entries.clone(),
78 worker_resolver,
79 ))
80 }
81}
82
83#[async_trait]
88impl pb::worker_service_server::WorkerService for Worker {
89 type CoordinatorChannelStream = BoxStream<'static, Result<pb::WorkerToCoordinatorMsg, Status>>;
90
91 async fn coordinator_channel(
92 &self,
93 request: Request<Streaming<pb::CoordinatorToWorkerMsg>>,
94 ) -> Result<Response<Self::CoordinatorChannelStream>, Status> {
95 let (metadata, _ext, mut body) = request.into_parts();
96
97 let msg = body
98 .message()
99 .await?
100 .ok_or_else(empty("Coordinator stream"))?
101 .inner
102 .ok_or_else(missing("CoordinatorToWorkerMsg.inner"))?;
103 let pb::coordinator_to_worker_msg::Inner::SetPlanRequest(set_plan_request) = msg else {
104 return Err(Status::invalid_argument(
105 "First Coordinator to Worker message must be SetPlanRequest",
106 ));
107 };
108
109 let set_plan_request = decode_set_plan_request(set_plan_request)?;
110
111 let input_stream = body
112 .map_err(map_status_to_datafusion_error)
113 .map(move |msg| {
114 decode_coordinator_to_worker_msg(msg?).map_err(map_status_to_datafusion_error)
115 })
116 .boxed();
117
118 let output_stream = self
119 .coordinator_channel(metadata.into_headers(), set_plan_request, input_stream)
120 .await
121 .map_err(datafusion_error_to_tonic_status)?
122 .map(|msg| match msg {
123 Ok(msg) => encode_worker_to_coordinator_msg(msg),
124 Err(err) => Err(datafusion_error_to_tonic_status(err)),
125 })
126 .boxed();
127
128 Ok(Response::new(output_stream))
129 }
130
131 type ExecuteTaskStream = BoxStream<'static, Result<FlightData, Status>>;
132
133 async fn execute_task(
134 &self,
135 request: Request<pb::ExecuteTaskRequest>,
136 ) -> Result<Response<Self::ExecuteTaskStream>, Status> {
137 let body = request.into_inner();
138 let request = decode_execute_task_request(body).await?;
139 let partition_range = request.target_partition_start..request.target_partition_end;
140
141 let (arrow_streams, task_ctx) = self
142 .execute_task(request)
143 .await
144 .map_err(datafusion_error_to_tonic_status)?;
145
146 let d_cfg = DistributedConfig::from_config_options(task_ctx.session_config().options())
147 .map_err(datafusion_error_to_tonic_status)?;
148
149 let compression = match d_cfg.compression.as_str() {
150 "lz4" => Some(CompressionType::LZ4_FRAME),
151 "zstd" => Some(CompressionType::ZSTD),
152 "none" => None,
153 v => Err(Status::invalid_argument(format!(
154 "Unknown compression type {v}"
155 )))?,
156 };
157 let mut flight_streams = Vec::with_capacity(arrow_streams.len());
158 for (partition, arrow_stream) in partition_range.zip(arrow_streams) {
159 let flight_stream =
160 build_flight_data_stream(arrow_stream, compression)?.map(move |msg| {
161 let flight_data = pb::FlightAppMetadata {
166 partition: partition as u64,
167 created_timestamp_unix_nanos: now_ns::<u64>(),
168 };
169 msg.map(|v| v.with_app_metadata(flight_data.encode_to_vec()))
170 });
171
172 flight_streams.push(flight_stream);
173 }
174
175 let memory_pool = Arc::clone(&task_ctx.runtime_env().memory_pool);
178 let stream = spawn_select_all(flight_streams, memory_pool, RECORD_BATCH_BUFFER_SIZE);
179
180 Ok(Response::new(Box::pin(stream.map_err(|err| match err {
181 FlightError::Tonic(status) => *status,
182 _ => Status::internal(format!("Error during flight stream: {err}")),
183 }))))
184 }
185
186 async fn get_worker_info(
187 &self,
188 _request: Request<pb::GetWorkerInfoRequest>,
189 ) -> Result<Response<pb::GetWorkerInfoResponse>, Status> {
190 Ok(Response::new(pb::GetWorkerInfoResponse {
191 version: self.version().to_string(),
192 }))
193 }
194}
195
196fn decode_coordinator_to_worker_msg(
197 msg: pb::CoordinatorToWorkerMsg,
198) -> Result<CoordinatorToWorkerMsg, Status> {
199 Ok(
200 match msg
201 .inner
202 .ok_or_else(missing("CoordinatorToWorkerMsg.inner"))?
203 {
204 pb::coordinator_to_worker_msg::Inner::SetPlanRequest(_) => {
205 return Err(Status::invalid_argument(
206 "SetPlanRequest must be the first coordinator message",
207 ));
208 }
209 pb::coordinator_to_worker_msg::Inner::WorkUnitBatch(batch) => {
210 CoordinatorToWorkerMsg::WorkUnitBatch(decode_work_unit_batch(batch)?)
211 }
212 pb::coordinator_to_worker_msg::Inner::WorkUnitEos(_) => {
213 CoordinatorToWorkerMsg::WorkUnitEos
214 }
215 },
216 )
217}
218
219fn decode_set_plan_request(request: pb::SetPlanRequest) -> Result<SetPlanRequest, Status> {
220 Ok(SetPlanRequest {
221 task_key: decode_task_key(request.task_key.ok_or_else(missing("task_key"))?)?,
222 task_count: request.task_count as usize,
223 plan: MaybeEncoded::Encoded(request.plan_proto),
224 work_unit_feed_declarations: request
225 .work_unit_feed_declarations
226 .into_iter()
227 .map(decode_work_unit_feed_declaration)
228 .collect::<Result<_, _>>()?,
229 target_worker_url: parse_url(&request.target_worker_url, "target_worker_url")?,
230 query_start_time_ns: request.query_start_time_ns as usize,
231 })
232}
233
234async fn decode_execute_task_request(
235 request: pb::ExecuteTaskRequest,
236) -> Result<ExecuteTaskRequest, Status> {
237 Ok(ExecuteTaskRequest {
238 task_key: decode_task_key(request.task_key.ok_or_else(missing("task_key"))?)?,
239 target_partition_start: request.target_partition_start as usize,
240 target_partition_end: request.target_partition_end as usize,
241 producer_head: decode_producer_head(
242 request.producer_head.ok_or_else(missing("producer_head"))?,
243 ),
244 })
245}
246
247pub(super) fn decode_producer_head(proto: pb::execute_task_request::ProducerHead) -> ProducerHead {
248 match proto {
249 pb::execute_task_request::ProducerHead::None(_) => ProducerHead::None,
250 pb::execute_task_request::ProducerHead::Broadcast(v) => ProducerHead::BroadcastExec {
251 output_partitions: v.output_partitions as usize,
252 },
253 pb::execute_task_request::ProducerHead::Repartition(v) => ProducerHead::RepartitionExec {
254 partitioning: MaybeEncoded::Encoded(v.partitioning),
255 },
256 }
257}
258
259fn encode_worker_to_coordinator_msg(
260 msg: WorkerToCoordinatorMsg,
261) -> Result<pb::WorkerToCoordinatorMsg, Status> {
262 Ok(pb::WorkerToCoordinatorMsg {
263 inner: Some(match msg {
264 WorkerToCoordinatorMsg::TaskMetrics(task_metrics) => {
265 pb::worker_to_coordinator_msg::Inner::TaskMetrics(encode_task_metrics(
266 task_metrics,
267 )?)
268 }
269 WorkerToCoordinatorMsg::LoadInfo(load_info) => {
270 pb::worker_to_coordinator_msg::Inner::LoadInfo(encode_load_info(load_info))
271 }
272 WorkerToCoordinatorMsg::LoadInfoEos => {
273 pb::worker_to_coordinator_msg::Inner::LoadInfoEos(true)
274 }
275 }),
276 })
277}
278
279fn encode_task_metrics(task_metrics: TaskMetrics) -> Result<pb::TaskMetrics, Status> {
280 Ok(pb::TaskMetrics {
281 pre_order_plan_metrics: task_metrics
282 .pre_order_plan_metrics
283 .into_iter()
284 .map(|metrics_set| {
285 df_metrics_set_to_proto(&metrics_set).map_err(datafusion_error_to_tonic_status)
286 })
287 .collect::<Result<_, _>>()?,
288 task_metrics: Some(
289 df_metrics_set_to_proto(&task_metrics.task_metrics)
290 .map_err(datafusion_error_to_tonic_status)?,
291 ),
292 })
293}
294
295fn encode_load_info(load_info: LoadInfo) -> pb::LoadInfo {
296 pb::LoadInfo {
297 partition: load_info.partition as u64,
298 rows_ready: load_info.rows_ready as u64,
299 per_column_bytes_ready: load_info
300 .per_column_bytes_ready
301 .into_iter()
302 .map(|bytes| bytes as u64)
303 .collect(),
304 per_column_ndv_percentage: load_info.per_column_ndv_percentage,
305 per_column_null_percentage: load_info.per_column_null_percentage,
306 rows_pulled_from_leaf: load_info.rows_pulled_from_leaf as u64,
307 reached_eos: load_info.reached_eos,
308 }
309}
310
311fn decode_work_unit_batch(batch: pb::WorkUnitBatch) -> Result<WorkUnitBatch, Status> {
312 Ok(WorkUnitBatch {
313 batch: batch
314 .batch
315 .into_iter()
316 .map(decode_work_unit)
317 .collect::<Result<_, _>>()?,
318 })
319}
320
321fn decode_work_unit(work_unit: pb::WorkUnit) -> Result<WorkUnitMsg, Status> {
322 Ok(WorkUnitMsg {
323 id: deserialize_uuid(&work_unit.id).map_err(datafusion_error_to_tonic_status)?,
324 partition: work_unit.partition as usize,
325 body: MaybeEncoded::Encoded(work_unit.body),
326 created_timestamp_unix_nanos: work_unit.created_timestamp_unix_nanos as usize,
327 sent_timestamp_unix_nanos: work_unit.sent_timestamp_unix_nanos as usize,
328 received_timestamp_unix_nanos: work_unit.received_timestamp_unix_nanos as usize,
329 processed_timestamp_unix_nanos: work_unit.processed_timestamp_unix_nanos as usize,
330 })
331}
332
333fn decode_work_unit_feed_declaration(
334 declaration: pb::set_plan_request::WorkUnitFeedDeclaration,
335) -> Result<WorkUnitFeedDeclaration, Status> {
336 Ok(WorkUnitFeedDeclaration {
337 id: deserialize_uuid(&declaration.id).map_err(datafusion_error_to_tonic_status)?,
338 partitions: declaration.partitions as usize,
339 })
340}
341
342fn decode_task_key(task_key: pb::TaskKey) -> Result<TaskKey, Status> {
343 Ok(TaskKey {
344 query_id: deserialize_uuid(&task_key.query_id).map_err(datafusion_error_to_tonic_status)?,
345 stage_id: task_key.stage_id as usize,
346 task_number: task_key.task_number as usize,
347 })
348}
349
350fn parse_url(value: &str, field: &'static str) -> Result<Url, Status> {
351 Url::parse(value)
352 .map_err(|err| Status::invalid_argument(format!("Invalid field '{field}': {err}")))
353}
354
355fn empty(stream_name: &'static str) -> impl FnOnce() -> Status {
356 move || Status::invalid_argument(format!("Empty {stream_name}"))
357}
358
359fn missing(field: &'static str) -> impl FnOnce() -> Status {
360 move || Status::invalid_argument(format!("Missing field '{field}'"))
361}
362
363fn build_flight_data_stream(
364 stream: SendableRecordBatchStream,
365 compression_type: Option<CompressionType>,
366) -> datafusion::common::Result<FlightDataEncoder, Status> {
367 let stream = FlightDataEncoderBuilder::new()
368 .with_options(
369 IpcWriteOptions::default()
370 .try_with_compression(compression_type)
371 .map_err(|err| Status::internal(err.to_string()))?,
372 )
373 .with_schema(stream.schema())
374 .with_dictionary_handling(DictionaryHandling::Resend)
383 .with_max_flight_data_size(usize::MAX)
392 .build(
393 stream
394 .and_then(|rb| std::future::ready(garbage_collect_arrays(rb)))
396 .map_err(|err| FlightError::Tonic(Box::new(datafusion_error_to_tonic_status(err)))),
397 );
398 Ok(stream)
399}
400
401fn garbage_collect_arrays(
409 batch: RecordBatch,
410) -> datafusion::common::Result<RecordBatch, DataFusionError> {
411 let (schema, arrays, row_count) = batch.into_parts();
412
413 let arrays = arrays
414 .into_iter()
415 .map(|array| {
416 if let Some(array) = array.as_any_dictionary_opt() {
417 garbage_collect_any_dictionary(array)
418 } else if let Some(array) = array.as_string_view_opt() {
419 Ok(Arc::new(array.gc()) as Arc<dyn Array>)
420 } else if let Some(array) = array.as_binary_view_opt() {
421 Ok(Arc::new(array.gc()) as Arc<dyn Array>)
422 } else {
423 Ok(array)
424 }
425 })
426 .collect::<datafusion::common::Result<Vec<_>, _>>()?;
427
428 Ok(RecordBatch::try_new_with_options(
429 schema,
430 arrays,
431 &RecordBatchOptions::new().with_row_count(Some(row_count)),
432 )?)
433}