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/04/16 11:53:17 UTC

(datafusion) 01/01: fix: Remove nested async block causing Stacked Borrows violation in PushDecoderStreamState (#21663)

This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch gh-readonly-queue/main/pr-21663-7bfa3fbdf8e0eedfacb844048b3ee764155a1670
in repository https://gitbox.apache.org/repos/asf/datafusion.git

commit 8b47f45ad216e6275ea6cc728d807dc6e34e05ff
Author: Matt Butrovich <mb...@users.noreply.github.com>
AuthorDate: Thu Apr 16 07:52:54 2026 -0400

    fix: Remove nested async block causing Stacked Borrows violation in PushDecoderStreamState (#21663)
    
    ## Which issue does this PR close?
    
    - Closes #21662.
    
    ## Rationale for this change
    
    Miri detects a Stacked Borrows violation in
    `PushDecoderStreamState::transition`. A nested `async` block captures
    `&mut self` as a single opaque mutable reference. At the `.await` on
    `get_byte_ranges`, the future yields, and the `Unique` tag on the borrow
    stack is invalidated by a `SharedReadOnly` retag. When the future
    resumes, `push_ranges` attempts a two-phase retag through the
    now-invalidated tag.
    
    This was found by [Apache DataFusion
    Comet](https://github.com/apache/datafusion-comet), which runs Miri in
    CI.
    
    ## What changes are included in this PR?
    
    Remove the nested `async` block in the `NeedsData` arm of
    `PushDecoderStreamState::transition` and inline the IO
    (`get_byte_ranges`) and CPU (`push_ranges`) operations as separate
    statements. Since `transition` is already an `async fn`, the `.await`
    works directly in the loop body. Without the nested block, the compiler
    can split the borrows of `self.reader` and `self.decoder` into disjoint
    field borrows, keeping the borrow stack valid across the yield point.
    
    Also removes the now-unused `parquet::errors::ParquetError` import.
    
    ## Are these changes tested?
    
    Covered by existing parquet reader tests. The original violation was
    caught by Miri, which DataFusion does not currently run in CI.
    
    ## Are there any user-facing changes?
    
    No.
---
 datafusion/datasource-parquet/src/opener.rs | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/datafusion/datasource-parquet/src/opener.rs b/datafusion/datasource-parquet/src/opener.rs
index 6d1c22c0b0..72549d5b87 100644
--- a/datafusion/datasource-parquet/src/opener.rs
+++ b/datafusion/datasource-parquet/src/opener.rs
@@ -75,7 +75,6 @@ use parquet::arrow::parquet_column;
 use parquet::arrow::push_decoder::{ParquetPushDecoder, ParquetPushDecoderBuilder};
 use parquet::basic::Type;
 use parquet::bloom_filter::Sbbf;
-use parquet::errors::ParquetError;
 use parquet::file::metadata::{PageIndexPolicy, ParquetMetaDataReader};
 
 /// Stateless Parquet morselizer implementation.
@@ -1253,13 +1252,23 @@ impl PushDecoderStreamState {
         loop {
             match self.decoder.try_decode() {
                 Ok(DecodeResult::NeedsData(ranges)) => {
-                    let fetch = async {
-                        let data = self.reader.get_byte_ranges(ranges.clone()).await?;
-                        self.decoder.push_ranges(ranges, data)?;
-                        Ok::<_, ParquetError>(())
-                    };
-                    if let Err(e) = fetch.await {
-                        return Some(Err(DataFusionError::from(e)));
+                    // IO (get_byte_ranges) and CPU (push_ranges) are still
+                    // decoupled — they just can't live in a nested async block
+                    // because that captures `&mut self` as one opaque borrow,
+                    // which violates Stacked Borrows across the yield point.
+                    // Inlining lets the compiler split the disjoint field borrows.
+                    let data = self
+                        .reader
+                        .get_byte_ranges(ranges.clone())
+                        .await
+                        .map_err(DataFusionError::from);
+                    match data {
+                        Ok(data) => {
+                            if let Err(e) = self.decoder.push_ranges(ranges, data) {
+                                return Some(Err(DataFusionError::from(e)));
+                            }
+                        }
+                        Err(e) => return Some(Err(e)),
                     }
                 }
                 Ok(DecodeResult::Data(batch)) => {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org