geoffreyclaude commented on code in PR #26242:
URL: https://github.com/apache/datafusion/pull/26242#discussion_r4279842931


##########
datafusion/physical-plan/src/sorts/sort.rs:
##########
@@ -716,6 +752,73 @@ impl ExternalSorter {
             .build()
     }
 
+    /// Prepare runs concurrently and return their streams in the original run 
order.
+    async fn prepare_sort_runs(
+        &mut self,
+        runs: Vec<RecordBatch>,
+        concurrency: &SortConcurrency,
+    ) -> Result<Vec<SendableRecordBatchStream>> {
+        let mut streams: Vec<_> = (0..runs.len()).map(|_| None).collect();
+        let jobs = runs.into_iter().enumerate().map(move |(index, batch)| {
+            let reservation = self
+                .reservation
+                .split(get_reserved_bytes_for_record_batch(&batch)?);
+            let stream = self.sort_batch_stream(batch, reservation)?;
+            let permits = Arc::clone(&concurrency.permits);
+            Ok::<_, DataFusionError>(async move {
+                let stream = Self::prepare_sort_run(stream, permits).await?;
+                Ok((index, stream))
+            })
+        });
+        let mut jobs =
+            
futures::stream::iter(jobs).try_buffer_unordered(concurrency.limit);
+        while let Some((index, stream)) = jobs.try_next().await? {
+            streams[index] = Some(stream);
+        }
+        streams
+            .into_iter()
+            .collect::<Option<Vec<_>>>()
+            .ok_or_else(|| internal_datafusion_err!("Missing prepared sort 
run"))
+    }
+
+    /// Sort one run through its first batch and release its slot before 
merging.
+    async fn prepare_sort_run(
+        mut stream: SendableRecordBatchStream,
+        permits: Arc<Semaphore>,
+    ) -> Result<SendableRecordBatchStream> {
+        // Acquire before spawning so queued runs do not become Tokio tasks.
+        let permit = permits
+            .acquire_owned()
+            .await
+            .map_err(|e| internal_datafusion_err!("Sort concurrency closed: 
{e}"))?;
+        SpawnedTask::spawn(async move {
+            // Match the buffered producer's first-batch handoff so spill
+            // workspace can be reused while the remaining runs are sorted.
+            let first = stream.try_next().await?.map(Ok);
+            let schema = stream.schema();
+            let ready = Box::pin(RecordBatchStreamAdapter::new(
+                schema,
+                futures::stream::unfold(
+                    (first, stream),
+                    |(buffered, mut stream)| async move {
+                        let batch = buffered?;
+                        // Sorting materializes every chunk on the first poll.
+                        // Refill the one-batch buffer before handing off its
+                        // current batch, returning workspace before the merge
+                        // reserves memory for that batch and its cursor.
+                        let next = stream.next().await;
+                        Some((batch, (next, stream)))

Review Comment:
   Good catch. Preparation now keeps all sorted output reserved until merging 
starts. During merging, the one-batch lookahead still temporarily releases the 
prefetched batch’s reservation so the merge can reuse that workspace.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to