You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/06/30 07:38:54 UTC
(datafusion) 01/01: fix: surface BufferExec input panics instead of silently truncating output (#23243)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-23243-01bf68cdc46ffbf4c226e760b0ba8a63fdb89a6b
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit 6ef3a3bdccdb3a6d0a75079d0be7d1d5deeec1ba
Author: Wenqi Mou <we...@gmail.com>
AuthorDate: Tue Jun 30 03:38:30 2026 -0400
fix: surface BufferExec input panics instead of silently truncating output (#23243)
## Which issue does this close?
Closes #23242.
## Rationale for this change
When the input to a `BufferExec` panics, the panic unwinds the
background producer task (`MemoryBufferedStream`), tokio catches it at
the task boundary, and the dropped sender looks like a clean
end-of-stream to the consumer. A panic isn't a `Result::Err`, so it also
skips the `batch_tx.send(Err(..))` path — the partition gets silently
truncated and the query returns partial results instead of failing. More
detail in #23242.
## What changes are included in this PR?
Catch the panic at the input poll inside `MemoryBufferedStream` and
forward it as a `DataFusionError` over the channel the consumer already
drains, so it propagates instead of being swallowed.
I went with surfacing it as an error rather than re-raising via
`join_unwind` (the way `RecordBatchReceiverStream` does), since the
consumer already handles `Some(Err(..))` and this fails only the query.
Happy to switch to a re-raise if that's preferred for consistency.
## Are these changes tested?
Yes — added `panic_in_input_is_propagated`, which feeds a stream that
panics partway through and asserts the buffered stream yields an error
instead of finishing cleanly.
## Are there any user-facing changes?
A query whose input panics under a `BufferExec` now fails with an error
instead of silently returning truncated results. No API changes.
---
datafusion/physical-plan/src/buffer.rs | 49 +++++++++++++++++++++++++++++-----
1 file changed, 43 insertions(+), 6 deletions(-)
diff --git a/datafusion/physical-plan/src/buffer.rs b/datafusion/physical-plan/src/buffer.rs
index 6d1fb69635..5e220b7e48 100644
--- a/datafusion/physical-plan/src/buffer.rs
+++ b/datafusion/physical-plan/src/buffer.rs
@@ -41,9 +41,10 @@ use datafusion_physical_expr_common::metrics::{
};
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
-use futures::{Stream, StreamExt, TryStreamExt};
+use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use pin_project_lite::pin_project;
use std::fmt;
+use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -337,11 +338,24 @@ impl<T: Send + SizedMessage + 'static> MemoryBufferedStream<T> {
let item_or_err = tokio::select! {
biased;
_ = batch_tx.closed() => break,
- item_or_err = input.next() => {
- let Some(item_or_err) = item_or_err else {
- break; // stream finished
- };
- item_or_err
+ // Catch a panic in the input poll so it surfaces as a stream error
+ // instead of dropping `batch_tx` and looking like a clean EOF.
+ polled = AssertUnwindSafe(input.next()).catch_unwind() => {
+ match polled {
+ Ok(Some(item_or_err)) => item_or_err,
+ Ok(None) => break, // stream finished
+ Err(panic) => {
+ let msg = panic
+ .downcast_ref::<&str>()
+ .map(|s| s.to_string())
+ .or_else(|| panic.downcast_ref::<String>().cloned())
+ .unwrap_or_else(|| "unknown panic".to_string());
+ let _ = batch_tx.send(internal_err!(
+ "BufferExec input stream panicked: {msg}"
+ ));
+ break;
+ }
+ }
}
};
@@ -554,6 +568,29 @@ mod tests {
Ok(())
}
+ #[tokio::test]
+ async fn panic_in_input_is_propagated() -> Result<(), Box<dyn Error>> {
+ // A panic while polling the input must surface as a stream error, not a
+ // silent end-of-stream that drops the rest of the partition's output.
+ let input = futures::stream::iter([1, 2, 3, 4]).map(|v| {
+ if v == 3 {
+ panic!("boom on 3");
+ }
+ Ok(v)
+ });
+ let (_, res) = memory_pool_and_reservation();
+
+ let mut buffered = MemoryBufferedStream::new(input, 10, res);
+ wait_for_buffering().await;
+
+ pull_ok_msg(&mut buffered).await?;
+ pull_ok_msg(&mut buffered).await?;
+ let err = pull_err_msg(&mut buffered).await?;
+ assert_contains!(err.to_string(), "panicked");
+
+ Ok(())
+ }
+
#[tokio::test]
async fn memory_gets_released_if_stream_drops() -> Result<(), Box<dyn Error>> {
let input = futures::stream::iter([1, 2, 3, 4]).map(Ok);
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org