Skip to content

Commit ba70127

Browse files
[ArrowFlight] Fix ArrowStream close() (#534)
1 parent ac8d46f commit ba70127

3 files changed

Lines changed: 64 additions & 3 deletions

File tree

rust/NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Bug Fixes
1010

11+
- **Arrow Flight — `close()` now propagates flush errors** (Beta): `ZerobusArrowStream::close()` previously swallowed a failed final `flush()` and always returned `Ok(())`, contradicting its documentation and diverging from the proto stream's `close()`. It now returns the flush error after still tearing down the stream and moving pending batches to the failed set (retrievable via `get_unacked_batches()`).
12+
1113
### Documentation
1214

1315
### Internal Changes

rust/sdk/src/arrow_stream.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,8 +1566,10 @@ impl ZerobusArrowStream {
15661566
"Closing Arrow Flight stream"
15671567
);
15681568

1569-
// Flush pending batches.
1570-
if let Err(e) = self.flush().await {
1569+
// Flush pending batches. Capture the result so the stream is still torn
1570+
// down on failure, then propagate it to the caller.
1571+
let flush_result = self.flush().await;
1572+
if let Err(e) = &flush_result {
15711573
warn!(
15721574
"Flush failed during close: {}. Moving pending batches to failed.",
15731575
e
@@ -1593,7 +1595,7 @@ impl ZerobusArrowStream {
15931595
}
15941596
}
15951597

1596-
Ok(())
1598+
flush_result
15971599
}
15981600

15991601
/// Returns all batches that were ingested but not acknowledged by the server.

rust/tests/src/arrow_tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,60 @@ mod arrow_flight_tests {
523523
Ok(())
524524
}
525525

526+
#[tokio::test]
527+
async fn test_close_propagates_flush_error() -> Result<(), Box<dyn std::error::Error>> {
528+
setup_tracing();
529+
info!("Starting test_close_propagates_flush_error");
530+
531+
let (mock_server, server_url) = start_mock_flight_server().await?;
532+
let schema = create_test_arrow_schema();
533+
534+
// Ack is delayed well past the flush timeout, so the flush performed by
535+
// close() times out while the stream is still open (no server error, so the
536+
// idempotent close guard does not short-circuit).
537+
mock_server
538+
.inject_responses(
539+
TABLE_NAME,
540+
vec![MockFlightResponse::BatchAck {
541+
ack_up_to_offset: 0,
542+
delay_ms: 5000,
543+
ack_up_to_records: 1,
544+
}],
545+
)
546+
.await;
547+
548+
let sdk = ZerobusSdk::builder()
549+
.endpoint(server_url.clone())
550+
.unity_catalog_url("https://mock-uc.com")
551+
.tls_config(Arc::new(NoTlsConfig))
552+
.build()?;
553+
554+
let mut stream = sdk
555+
.stream_builder()
556+
.table(TABLE_NAME)
557+
.headers_provider(Arc::new(TestHeadersProvider::default()))
558+
.arrow(schema.clone())
559+
.flush_timeout_ms(100)
560+
.build_arrow()
561+
.await?;
562+
563+
let batch = create_test_record_batch(schema, vec![1], vec![Some("unacked")]);
564+
let _offset = stream.ingest_batch(batch).await?;
565+
566+
let close_result = stream.close().await;
567+
assert!(
568+
close_result.is_err(),
569+
"close() must propagate the error when its flush fails"
570+
);
571+
572+
// The stream is still torn down, and the unacked batch is recoverable.
573+
assert!(stream.is_closed());
574+
let unacked = stream.get_unacked_batches().await?;
575+
assert_eq!(unacked.len(), 1, "Should have 1 unacked batch");
576+
577+
Ok(())
578+
}
579+
526580
#[tokio::test]
527581
async fn test_empty_flush() -> Result<(), Box<dyn std::error::Error>> {
528582
setup_tracing();
@@ -756,6 +810,9 @@ mod arrow_flight_tests {
756810
let offset2 = stream.ingest_batch(batch2).await?;
757811
assert!(stream.wait_for_offset(offset2).await.is_err());
758812

813+
// The permanent error already closed the stream, so close() short-circuits
814+
// to Ok via its idempotent guard; the unacked batch was moved to failed by
815+
// the error path, not by this close().
759816
let _ = stream.close().await;
760817

761818
let unacked = stream.get_unacked_batches().await?;

0 commit comments

Comments
 (0)