This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git
The following commit(s) were added to refs/heads/main by this push:
new bab58105 fix(arrow/flight): avoid blocking after stream cancellation
(#989)
bab58105 is described below
commit bab58105e336430c782bba4a04ecfc1a4915f58b
Author: Minh Vu <[email protected]>
AuthorDate: Thu Jul 23 18:09:43 2026 +0200
fix(arrow/flight): avoid blocking after stream cancellation (#989)
### Rationale for this change
`StreamChunksFromReader` observes context cancellation while sending
record batches, but its final reader-error send still wrote directly to
the output channel. If the consumer stopped receiving after
cancellation, that send could block forever, preventing reader release
and channel closure. This is a follow-up to the cancellation handling
added in #615.
### What changes are included in this PR?
Race final error delivery against context cancellation, matching the
existing record-batch send behavior. Capture the reader error once
before delivery. Add a regression with an already-canceled context, an
unbuffered channel, and a reader that fails immediately.
### Are these changes tested?
Yes:
- `go test ./arrow/flight`
- `go test -race -count=1 ./arrow/flight`
### Are there any user-facing changes?
`StreamChunksFromReader` now returns promptly and releases its reader
when cancellation prevents final error delivery. Error delivery is
unchanged while the context remains active.
---
arrow/flight/flight_test.go | 37 +++++++++++++++++++++++++++++++++++++
arrow/flight/record_batch_reader.go | 7 +++++--
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/arrow/flight/flight_test.go b/arrow/flight/flight_test.go
index 8d75aac2..e6b8a25b 100644
--- a/arrow/flight/flight_test.go
+++ b/arrow/flight/flight_test.go
@@ -24,6 +24,7 @@ import (
"sync"
"sync/atomic"
"testing"
+ "time"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
@@ -635,6 +636,42 @@ func TestStreamChunksFromReader_OK(t *testing.T) {
}
+type immediateErrorRecordReader struct {
+ err error
+ released atomic.Bool
+}
+
+func (*immediateErrorRecordReader) Retain() {}
+func (r *immediateErrorRecordReader) Release() {
r.released.Store(true) }
+func (*immediateErrorRecordReader) Schema() *arrow.Schema { return
nil }
+func (*immediateErrorRecordReader) Next() bool { return
false }
+func (*immediateErrorRecordReader) RecordBatch() arrow.RecordBatch { return
nil }
+func (*immediateErrorRecordReader) Record() arrow.RecordBatch { return
nil }
+func (r *immediateErrorRecordReader) Err() error { return
r.err }
+
+func TestStreamChunksFromReader_CancellationWhileSendingError(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ rdr := &immediateErrorRecordReader{err: errors.New("read failed")}
+ ch := make(chan flight.StreamChunk)
+ done := make(chan struct{})
+ go func() {
+ flight.StreamChunksFromReader(ctx, rdr, ch)
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("StreamChunksFromReader blocked sending an error after
cancellation")
+ }
+
+ if !rdr.released.Load() {
+ t.Fatal("reader was not released")
+ }
+}
+
// TestStreamChunksFromReader_HandlesCancellation verifies that context
cancellation
// causes StreamChunksFromReader to exit cleanly and release the reader.
func TestStreamChunksFromReader_HandlesCancellation(t *testing.T) {
diff --git a/arrow/flight/record_batch_reader.go
b/arrow/flight/record_batch_reader.go
index e6990a57..8c9d3876 100644
--- a/arrow/flight/record_batch_reader.go
+++ b/arrow/flight/record_batch_reader.go
@@ -248,8 +248,11 @@ func StreamChunksFromReader(ctx context.Context, rdr
array.RecordReader, ch chan
}
if e, ok := rdr.(haserr); ok {
- if e.Err() != nil {
- ch <- StreamChunk{Err: e.Err()}
+ if err := e.Err(); err != nil {
+ select {
+ case ch <- StreamChunk{Err: err}:
+ case <-ctx.Done():
+ }
}
}
}