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 603dfb0a fix(arrow/ipc): avoid deadlock on compression errors (#1142)
603dfb0a is described below

commit 603dfb0aeacf8e678ddb73914b9e4b1c452a687a
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 14 18:07:27 2026 +0200

    fix(arrow/ipc): avoid deadlock on compression errors (#1142)
    
    ### Rationale for this change
    
    Parallel body compression sent worker errors through an unbuffered
    channel before the parent waited for the workers. A compressor failure
    could therefore leave the writer blocked forever, and the producer could
    continue sending work after the workers had stopped.
    
    ### What changes are included in this PR?
    
    Buffer the first worker error, make producer sends cancellation-aware,
    and add a regression test that uses a failing compressor with parallel
    compression.
    
    ### Are these changes tested?
    
    - `go test ./arrow/ipc`
    - `go test -race ./arrow/ipc -run
    TestRecordEncoderCompressionErrorDoesNotDeadlock`
    
    ### Are there any user-facing changes?
    
    Parallel IPC compression now returns compressor failures instead of
    hanging.
---
 arrow/ipc/writer.go      | 29 ++++++++++++++++++-----
 arrow/ipc/writer_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+), 6 deletions(-)

diff --git a/arrow/ipc/writer.go b/arrow/ipc/writer.go
index aebdb768..32edaf25 100644
--- a/arrow/ipc/writer.go
+++ b/arrow/ipc/writer.go
@@ -436,9 +436,11 @@ func (w *recordEncoder) compressBodyBuffers(p *Payload) 
error {
 
                n, err := codec.Write(p.body[idx].Bytes())
                if err != nil {
+                       buf.Release()
                        return err
                }
                if err := codec.Close(); err != nil {
+                       buf.Release()
                        return err
                }
 
@@ -471,7 +473,7 @@ func (w *recordEncoder) compressBodyBuffers(p *Payload) 
error {
        var (
                wg          sync.WaitGroup
                ch          = make(chan int)
-               errch       = make(chan error)
+               errch       = make(chan error, 1)
                ctx, cancel = context.WithCancel(context.Background())
        )
        defer cancel()
@@ -490,7 +492,10 @@ func (w *recordEncoder) compressBodyBuffers(p *Payload) 
error {
                                        }
 
                                        if err := compress(idx, codec); err != 
nil {
-                                               errch <- err
+                                               select {
+                                               case errch <- err:
+                                               default:
+                                               }
                                                cancel()
                                                return
                                        }
@@ -502,15 +507,24 @@ func (w *recordEncoder) compressBodyBuffers(p *Payload) 
error {
                }(workerID)
        }
 
+send:
        for idx := range p.body {
-               ch <- idx
+               select {
+               case ch <- idx:
+               case <-ctx.Done():
+                       break send
+               }
        }
 
        close(ch)
        wg.Wait()
-       close(errch)
 
-       return <-errch
+       select {
+       case err := <-errch:
+               return err
+       default:
+               return nil
+       }
 }
 
 func (w *recordEncoder) encode(p *Payload, rec arrow.RecordBatch) error {
@@ -528,7 +542,9 @@ func (w *recordEncoder) encode(p *Payload, rec 
arrow.RecordBatch) error {
                        return fmt.Errorf("%w: minSpaceSavings not in range 
[0,1]. Provided %.05f",
                                arrow.ErrInvalid, w.minSpaceSavings)
                }
-               w.compressBodyBuffers(p)
+               if err := w.compressBodyBuffers(p); err != nil {
+                       return err
+               }
        }
 
        // position for the start of a buffer relative to the passed frame of 
reference.
@@ -1153,6 +1169,7 @@ func GetRecordBatchPayload(batch arrow.RecordBatch, opts 
...Option) (Payload, er
 
        err := enc.Encode(&data, batch)
        if err != nil {
+               data.Release()
                return Payload{}, err
        }
 
diff --git a/arrow/ipc/writer_test.go b/arrow/ipc/writer_test.go
index 32bc5d90..dc13032b 100644
--- a/arrow/ipc/writer_test.go
+++ b/arrow/ipc/writer_test.go
@@ -25,6 +25,7 @@ import (
        "math"
        "strings"
        "testing"
+       "time"
 
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
@@ -46,6 +47,18 @@ type failingPayloadWriter struct {
 
 type shortWriteWriter struct{}
 
+type failingCompressor struct {
+       err error
+}
+
+func (failingCompressor) MaxCompressedLen(n int) int  { return n }
+func (failingCompressor) Reset(io.Writer)             {}
+func (f failingCompressor) Write([]byte) (int, error) { return 0, f.err }
+func (failingCompressor) Close() error                { return nil }
+func (failingCompressor) Type() flatbuf.CompressionType {
+       return flatbuf.CompressionTypeZSTD
+}
+
 func (shortWriteWriter) Write(p []byte) (int, error) {
        return len(p) - 1, io.ErrShortWrite
 }
@@ -333,6 +346,54 @@ func TestNewWriterWithMinSpaceSavings(t *testing.T) {
        assert.Equal(t, minSpaceSavings, writer.minSpaceSavings)
 }
 
+func TestRecordEncoderCompressionErrorDoesNotDeadlock(t *testing.T) {
+       want := errors.New("compression failed")
+       body := make([]*memory.Buffer, 64)
+       for i := range body {
+               body[i] = memory.NewBufferBytes([]byte("payload"))
+       }
+       payload := Payload{body: body}
+       defer payload.Release()
+
+       encoder := newRecordEncoder(memory.DefaultAllocator, 0, 
kMaxNestingDepth, true,
+               flatbuf.CompressionTypeZSTD, 2, 0, []compressor{
+                       failingCompressor{err: want},
+                       failingCompressor{err: want},
+               })
+
+       result := make(chan error, 1)
+       go func() {
+               result <- encoder.compressBodyBuffers(&payload)
+       }()
+
+       select {
+       case err := <-result:
+               require.ErrorIs(t, err, want)
+       case <-time.After(time.Second):
+               t.Fatal("compression did not return after timeout")
+       }
+}
+
+func TestRecordEncoderReturnsCompressionError(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       schema := arrow.NewSchema([]arrow.Field{{Name: "col", Type: 
arrow.PrimitiveTypes.Int8}}, nil)
+       builder := array.NewRecordBuilder(mem, schema)
+       defer builder.Release()
+       builder.Field(0).(*array.Int8Builder).Append(1)
+       record := builder.NewRecordBatch()
+       defer record.Release()
+
+       want := errors.New("compression failed")
+       encoder := newRecordEncoder(mem, 0, kMaxNestingDepth, true,
+               flatbuf.CompressionTypeZSTD, 1, 0, 
[]compressor{failingCompressor{err: want}})
+       var payload Payload
+       defer payload.Release()
+
+       require.ErrorIs(t, encoder.Encode(&payload, record), want)
+}
+
 func TestWriteWithCompressionAndMinSavings(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)

Reply via email to