-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Fix Possible Congestion Scenario in SortPreservingMergeExec
#12302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4382945
d14dde3
1567c0c
02cdbfe
eb068a0
93a9c7c
5640da8
07bf172
2e95923
9f32d2b
c8b32a5
4bdbafa
e0bf209
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,19 +18,23 @@ | |
| //! Merge that deals with an arbitrary size of streaming inputs. | ||
| //! This is an order-preserving merge. | ||
|
|
||
| use std::collections::VecDeque; | ||
| use std::pin::Pin; | ||
| use std::sync::Arc; | ||
| use std::task::{ready, Context, Poll}; | ||
|
|
||
| use crate::metrics::BaselineMetrics; | ||
| use crate::sorts::builder::BatchBuilder; | ||
| use crate::sorts::cursor::{Cursor, CursorValues}; | ||
| use crate::sorts::stream::PartitionedStream; | ||
| use crate::RecordBatchStream; | ||
|
|
||
| use arrow::datatypes::SchemaRef; | ||
| use arrow::record_batch::RecordBatch; | ||
| use datafusion_common::Result; | ||
| use datafusion_execution::memory_pool::MemoryReservation; | ||
|
|
||
| use futures::Stream; | ||
| use std::pin::Pin; | ||
| use std::sync::Arc; | ||
| use std::task::{ready, Context, Poll}; | ||
|
|
||
| /// A fallible [`PartitionedStream`] of [`Cursor`] and [`RecordBatch`] | ||
| type CursorStream<C> = Box<dyn PartitionedStream<Output = Result<(C, RecordBatch)>>>; | ||
|
|
@@ -86,7 +90,7 @@ pub(crate) struct SortPreservingMergeStream<C: CursorValues> { | |
| /// been updated | ||
| loser_tree_adjusted: bool, | ||
|
|
||
| /// target batch size | ||
| /// Target batch size | ||
| batch_size: usize, | ||
|
|
||
| /// Cursors for each input partition. `None` means the input is exhausted | ||
|
|
@@ -97,6 +101,12 @@ pub(crate) struct SortPreservingMergeStream<C: CursorValues> { | |
|
|
||
| /// number of rows produced | ||
| produced: usize, | ||
|
|
||
| /// This queue contains partition indices in order. When a partition is polled and returns `Poll::Ready`, | ||
| /// it is removed from the vector. If a partition returns `Poll::Pending`, it is moved to the end of the | ||
| /// vector to ensure the next iteration starts with a different partition, preventing the same partition | ||
| /// from being continuously polled. | ||
| uninitiated_partitions: VecDeque<usize>, | ||
| } | ||
|
|
||
| impl<C: CursorValues> SortPreservingMergeStream<C> { | ||
|
|
@@ -121,6 +131,7 @@ impl<C: CursorValues> SortPreservingMergeStream<C> { | |
| batch_size, | ||
| fetch, | ||
| produced: 0, | ||
| uninitiated_partitions: (0..stream_count).collect(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -154,14 +165,36 @@ impl<C: CursorValues> SortPreservingMergeStream<C> { | |
| if self.aborted { | ||
| return Poll::Ready(None); | ||
| } | ||
| // try to initialize the loser tree | ||
| // Once all partitions have set their corresponding cursors for the loser tree, | ||
| // we skip the following block. Until then, this function may be called multiple | ||
| // times and can return Poll::Pending if any partition returns Poll::Pending. | ||
| if self.loser_tree.is_empty() { | ||
| // Ensure all non-exhausted streams have a cursor from which | ||
| // rows can be pulled | ||
| for i in 0..self.streams.partitions() { | ||
| if let Err(e) = ready!(self.maybe_poll_stream(cx, i)) { | ||
| self.aborted = true; | ||
| return Poll::Ready(Some(Err(e))); | ||
| let remaining_partitions = self.uninitiated_partitions.clone(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This clone in particular shows up in traces |
||
| for i in remaining_partitions { | ||
| match self.maybe_poll_stream(cx, i) { | ||
| Poll::Ready(Err(e)) => { | ||
| self.aborted = true; | ||
| return Poll::Ready(Some(Err(e))); | ||
| } | ||
| Poll::Pending => { | ||
| // If a partition returns Poll::Pending, to avoid continuously polling it | ||
| // and potentially increasing upstream buffer sizes, we move it to the | ||
| // back of the polling queue. | ||
| if let Some(front) = self.uninitiated_partitions.pop_front() { | ||
| // This pop_front can never return `None`. | ||
| self.uninitiated_partitions.push_back(front); | ||
| } | ||
| // This function could remain in a pending state, so we manually wake it here. | ||
| // However, this approach can be investigated further to find a more natural way | ||
| // to avoid disrupting the runtime scheduler. | ||
| cx.waker().wake_by_ref(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if this usage has some side-effects or decrease performance, but I cannot wake the SPM poll again once it receives a pending from its first partition
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did some research -- see https://github.com/synnada-ai/datafusion-upstream/pull/34/files#r1743621057 I think calling But I share your concern that this will cause some sort of performance issue |
||
| return Poll::Pending; | ||
| } | ||
| _ => { | ||
| // If the polling result is Poll::Ready(Some(batch)) or Poll::Ready(None), | ||
| // we remove this partition from the queue so it is not polled again. | ||
| self.uninitiated_partitions.retain(|idx| *idx != i); | ||
| } | ||
| } | ||
| } | ||
| self.init_loser_tree(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.