datafusion_physical_plan/
coop.rs1use datafusion_common::config::ConfigOptions;
74use datafusion_common::tree_node::TreeNodeRecursion;
75use datafusion_physical_expr::PhysicalExpr;
76#[cfg(datafusion_coop = "tokio_fallback")]
77use futures::Future;
78use std::pin::Pin;
79use std::sync::Arc;
80use std::task::{Context, Poll};
81
82use crate::execution_plan::CardinalityEffect::{self, Equal};
83use crate::filter_pushdown::{
84 ChildPushdownResult, FilterDescription, FilterPushdownPhase,
85 FilterPushdownPropagation,
86};
87use crate::projection::ProjectionExec;
88use crate::statistics::{ChildStats, StatisticsArgs};
89use crate::{
90 ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties,
91 RecordBatchStream, ReplaceChildrenOptions, SendableRecordBatchStream,
92 SortOrderPushdownResult, validate_child_count,
93};
94use arrow::record_batch::RecordBatch;
95use arrow_schema::Schema;
96use datafusion_common::{Result, Statistics};
97use datafusion_execution::TaskContext;
98
99use crate::execution_plan::{SchedulingType, replace_children_if_necessary};
100use crate::stream::RecordBatchStreamAdapter;
101use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
102use futures::{Stream, StreamExt};
103
104pub struct CooperativeStream<T>
110where
111 T: RecordBatchStream + Unpin,
112{
113 inner: T,
114 #[cfg(datafusion_coop = "per_stream")]
115 budget: u8,
116}
117
118#[cfg(datafusion_coop = "per_stream")]
119const YIELD_FREQUENCY: u8 = 128;
121
122impl<T> CooperativeStream<T>
123where
124 T: RecordBatchStream + Unpin,
125{
126 pub fn new(inner: T) -> Self {
130 Self {
131 inner,
132 #[cfg(datafusion_coop = "per_stream")]
133 budget: YIELD_FREQUENCY,
134 }
135 }
136}
137
138impl<T> Stream for CooperativeStream<T>
139where
140 T: RecordBatchStream + Unpin,
141{
142 type Item = Result<RecordBatch>;
143
144 fn poll_next(
145 mut self: Pin<&mut Self>,
146 cx: &mut Context<'_>,
147 ) -> Poll<Option<Self::Item>> {
148 #[cfg(any(
149 datafusion_coop = "tokio",
150 not(any(
151 datafusion_coop = "tokio_fallback",
152 datafusion_coop = "per_stream"
153 ))
154 ))]
155 {
156 let coop = std::task::ready!(tokio::task::coop::poll_proceed(cx));
157 let value = self.inner.poll_next_unpin(cx);
158 if value.is_ready() {
159 coop.made_progress();
160 }
161 value
162 }
163
164 #[cfg(datafusion_coop = "tokio_fallback")]
165 {
166 if !tokio::task::coop::has_budget_remaining() {
169 cx.waker().wake_by_ref();
170 return Poll::Pending;
171 }
172
173 let value = self.inner.poll_next_unpin(cx);
174 if value.is_ready() {
175 let consume = tokio::task::coop::consume_budget();
181 let consume_ref = std::pin::pin!(consume);
182 let _ = consume_ref.poll(cx);
183 }
184 value
185 }
186
187 #[cfg(datafusion_coop = "per_stream")]
188 {
189 if self.budget == 0 {
190 self.budget = YIELD_FREQUENCY;
191 cx.waker().wake_by_ref();
192 return Poll::Pending;
193 }
194
195 let value = { self.inner.poll_next_unpin(cx) };
196
197 if value.is_ready() {
198 self.budget -= 1;
199 } else {
200 self.budget = YIELD_FREQUENCY;
201 }
202 value
203 }
204 }
205}
206
207impl<T> RecordBatchStream for CooperativeStream<T>
208where
209 T: RecordBatchStream + Unpin,
210{
211 fn schema(&self) -> Arc<Schema> {
212 self.inner.schema()
213 }
214}
215
216#[derive(Debug, Clone)]
220pub struct CooperativeExec {
221 input: Arc<dyn ExecutionPlan>,
222 properties: Arc<PlanProperties>,
223}
224
225impl CooperativeExec {
226 pub fn new(input: Arc<dyn ExecutionPlan>) -> Self {
228 let properties = PlanProperties::clone(input.properties())
229 .with_scheduling_type(SchedulingType::Cooperative)
230 .into();
231
232 Self { input, properties }
233 }
234
235 pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
237 &self.input
238 }
239}
240
241impl DisplayAs for CooperativeExec {
242 fn fmt_as(
243 &self,
244 _t: DisplayFormatType,
245 f: &mut std::fmt::Formatter<'_>,
246 ) -> std::fmt::Result {
247 write!(f, "CooperativeExec")
248 }
249}
250
251impl ExecutionPlan for CooperativeExec {
252 fn name(&self) -> &str {
253 "CooperativeExec"
254 }
255
256 fn schema(&self) -> Arc<Schema> {
257 self.input.schema()
258 }
259
260 fn properties(&self) -> &Arc<PlanProperties> {
261 &self.properties
262 }
263
264 fn maintains_input_order(&self) -> Vec<bool> {
265 vec![true; self.children().len()]
266 }
267
268 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
269 vec![&self.input]
270 }
271
272 fn apply_expressions(
273 &self,
274 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
275 ) -> Result<TreeNodeRecursion> {
276 Ok(TreeNodeRecursion::Continue)
277 }
278
279 fn replace_children(
280 self: Arc<Self>,
281 mut children: Vec<Arc<dyn ExecutionPlan>>,
282 options: ReplaceChildrenOptions,
283 ) -> Result<Arc<dyn ExecutionPlan>> {
284 validate_child_count!(self, children);
285 match options.children_properties {
286 ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
287 input: children.swap_remove(0),
288 ..Self::clone(&*self)
289 })),
290 ChildrenPropertiesMode::Recompute => {
291 Ok(Arc::new(CooperativeExec::new(children.swap_remove(0))))
292 }
293 }
294 }
295
296 fn with_new_children(
297 self: Arc<Self>,
298 children: Vec<Arc<dyn ExecutionPlan>>,
299 ) -> Result<Arc<dyn ExecutionPlan>> {
300 self.replace_children(
301 children,
302 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
303 )
304 }
305
306 fn with_new_children_and_same_properties(
307 self: Arc<Self>,
308 children: Vec<Arc<dyn ExecutionPlan>>,
309 ) -> Result<Arc<dyn ExecutionPlan>> {
310 self.replace_children(
311 children,
312 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
313 )
314 }
315
316 fn execute(
317 &self,
318 partition: usize,
319 task_ctx: Arc<TaskContext>,
320 ) -> Result<SendableRecordBatchStream> {
321 let child_stream = self.input.execute(partition, task_ctx)?;
322 Ok(make_cooperative(child_stream))
323 }
324
325 fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
326 vec![ChildStats::At(partition)]
327 }
328
329 fn statistics_from_inputs(
330 &self,
331 input_stats: &[Arc<Statistics>],
332 _args: &StatisticsArgs,
333 ) -> Result<Arc<Statistics>> {
334 Ok(Arc::clone(&input_stats[0]))
335 }
336
337 fn supports_limit_pushdown(&self) -> bool {
338 true
339 }
340
341 fn cardinality_effect(&self) -> CardinalityEffect {
342 Equal
343 }
344
345 fn try_swapping_with_projection(
346 &self,
347 projection: &ProjectionExec,
348 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
349 match self.input.try_swapping_with_projection(projection)? {
350 Some(new_input) => Ok(Some(replace_children_if_necessary(
351 Arc::new(self.clone()),
352 vec![new_input],
353 )?)),
354 None => Ok(None),
355 }
356 }
357
358 fn gather_filters_for_pushdown(
359 &self,
360 _phase: FilterPushdownPhase,
361 parent_filters: Vec<Arc<dyn PhysicalExpr>>,
362 _config: &ConfigOptions,
363 ) -> Result<FilterDescription> {
364 FilterDescription::from_children(parent_filters, &self.children())
365 }
366
367 fn handle_child_pushdown_result(
368 &self,
369 _phase: FilterPushdownPhase,
370 child_pushdown_result: ChildPushdownResult,
371 _config: &ConfigOptions,
372 ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
373 Ok(FilterPushdownPropagation::if_all(child_pushdown_result))
374 }
375
376 fn try_pushdown_sort(
377 &self,
378 order: &[PhysicalSortExpr],
379 ) -> Result<SortOrderPushdownResult<Arc<dyn ExecutionPlan>>> {
380 let child = self.input();
381
382 match child.try_pushdown_sort(order)? {
383 SortOrderPushdownResult::Exact { inner } => {
384 let new_exec =
385 replace_children_if_necessary(Arc::new(self.clone()), vec![inner])?;
386 Ok(SortOrderPushdownResult::Exact { inner: new_exec })
387 }
388 SortOrderPushdownResult::Inexact { inner } => {
389 let new_exec =
390 replace_children_if_necessary(Arc::new(self.clone()), vec![inner])?;
391 Ok(SortOrderPushdownResult::Inexact { inner: new_exec })
392 }
393 SortOrderPushdownResult::Unsupported => {
394 Ok(SortOrderPushdownResult::Unsupported)
395 }
396 }
397 }
398
399 #[cfg(feature = "proto")]
400 fn try_to_proto(
401 &self,
402 ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
403 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
404 use datafusion_proto_models::protobuf;
405 let input = ctx.encode_child(self.input())?;
406 Ok(Some(protobuf::PhysicalPlanNode {
407 physical_plan_type: Some(
408 protobuf::physical_plan_node::PhysicalPlanType::Cooperative(Box::new(
409 protobuf::CooperativeExecNode {
410 input: Some(Box::new(input)),
411 },
412 )),
413 ),
414 }))
415 }
416}
417
418#[cfg(feature = "proto")]
419impl CooperativeExec {
420 pub fn try_from_proto(
426 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
427 ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
428 ) -> Result<Arc<dyn ExecutionPlan>> {
429 use datafusion_proto_models::protobuf;
430 let cooperative = crate::expect_plan_variant!(
431 node,
432 protobuf::physical_plan_node::PhysicalPlanType::Cooperative,
433 "CooperativeExec",
434 );
435 let input = ctx.decode_required_child(
436 cooperative.input.as_deref(),
437 "CooperativeExec",
438 "input",
439 )?;
440 Ok(Arc::new(CooperativeExec::new(input)))
441 }
442}
443
444pub fn cooperative<T>(stream: T) -> CooperativeStream<T>
448where
449 T: RecordBatchStream + Unpin + Send + 'static,
450{
451 CooperativeStream::new(stream)
452}
453
454pub fn make_cooperative(stream: SendableRecordBatchStream) -> SendableRecordBatchStream {
460 Box::pin(cooperative(RecordBatchStreamAdapter::new(
462 stream.schema(),
463 stream,
464 )))
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 use arrow_schema::SchemaRef;
472
473 use futures::stream;
474
475 const TASK_BUDGET: usize = 128;
477
478 fn make_empty_batches(n: usize) -> SendableRecordBatchStream {
480 let schema: SchemaRef = Arc::new(Schema::empty());
481 let schema_for_stream = Arc::clone(&schema);
482
483 let s =
484 stream::iter((0..n).map(move |_| {
485 Ok(RecordBatch::new_empty(Arc::clone(&schema_for_stream)))
486 }));
487
488 Box::pin(RecordBatchStreamAdapter::new(schema, s))
489 }
490
491 #[tokio::test]
492 async fn yield_less_than_threshold() -> Result<()> {
493 let count = TASK_BUDGET - 10;
494 let inner = make_empty_batches(count);
495 let out = make_cooperative(inner).collect::<Vec<_>>().await;
496 assert_eq!(out.len(), count);
497 Ok(())
498 }
499
500 #[tokio::test]
501 async fn yield_equal_to_threshold() -> Result<()> {
502 let count = TASK_BUDGET;
503 let inner = make_empty_batches(count);
504 let out = make_cooperative(inner).collect::<Vec<_>>().await;
505 assert_eq!(out.len(), count);
506 Ok(())
507 }
508
509 #[tokio::test]
510 async fn yield_more_than_threshold() -> Result<()> {
511 let count = TASK_BUDGET + 20;
512 let inner = make_empty_batches(count);
513 let out = make_cooperative(inner).collect::<Vec<_>>().await;
514 assert_eq!(out.len(), count);
515 Ok(())
516 }
517}