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 0702f072 fix(compute): discard results after cancellation (#1278)
0702f072 is described below
commit 0702f0724e01d24b6f6db3b8eba80b9e529dc454
Author: Matt Topol <[email protected]>
AuthorDate: Thu Sep 3 11:09:05 2026 -0400
fix(compute): discard results after cancellation (#1278)
### Rationale for this change
`WrapResults` selects between result delivery and context cancellation.
When cancellation and channel closure become ready together, the channel
branch can win and return a partial result from a canceled
`CallFunction`. This has caused intermittent failures of the
caller-cancellation tests on Windows, macOS, and release verification.
### What changes are included in this PR?
- Discard and release scalar and vector partial outputs on cancellation
- Recheck cancellation after receiving output or channel closure
- Add direct coverage for releasing a single scalar output
- Release unexpected results in cancellation tests so regressions report
the primary assertion rather than a secondary allocator leak
### Are these changes tested?
- `go test ./arrow/compute -run
'Test(ScalarExecutorWrapResults|CallFunctionPreservesCallerCancellation)'
-count=100`
- `go test ./arrow/compute`
- pre-commit hooks
### Are there any user-facing changes?
Canceled compute calls now deterministically return no result instead of
occasionally returning a partial result.
Signed-off-by: Matt Topol <[email protected]>
---
arrow/compute/exec_test.go | 12 +++++++++
arrow/compute/executor.go | 58 ++++++++++++++++++++++++++++++++----------
arrow/compute/executor_test.go | 27 ++++++++++++++++++++
3 files changed, 83 insertions(+), 14 deletions(-)
diff --git a/arrow/compute/exec_test.go b/arrow/compute/exec_test.go
index 6f32abd3..65007dd5 100644
--- a/arrow/compute/exec_test.go
+++ b/arrow/compute/exec_test.go
@@ -169,6 +169,9 @@ func TestCallFunctionPreservesCallerCancellation(t
*testing.T) {
case <-time.After(time.Second):
t.Fatal("CallFunction did not stop after caller cancellation")
}
+ if result != nil {
+ defer result.Release()
+ }
require.Nil(t, result)
require.ErrorIs(t, callErr, cancellationErr)
}
@@ -228,6 +231,9 @@ func
TestCallFunctionReleasesPartialResultOnCallerCancellation(t *testing.T) {
t.Fatal("CallFunction did not stop after caller cancellation")
}
+ if result != nil {
+ defer result.Release()
+ }
require.Nil(t, result)
require.ErrorIs(t, callErr, cancellationErr)
}
@@ -319,6 +325,9 @@ func TestCallFunctionDrainsResultsAfterCallerCancellation(t
*testing.T) {
case <-time.After(time.Second):
t.Fatal("CallFunction did not finish after execution was
released")
}
+ if result != nil {
+ defer result.Release()
+ }
require.Nil(t, result)
require.ErrorIs(t, callErr, cancellationErr)
}
@@ -370,6 +379,9 @@ func
TestCallFunctionPreservesCallerCancellationForVectorFunction(t *testing.T)
case <-time.After(time.Second):
t.Fatal("vector CallFunction did not stop after caller
cancellation")
}
+ if result != nil {
+ defer result.Release()
+ }
require.Nil(t, result)
require.ErrorIs(t, callErr, cancellationErr)
}
diff --git a/arrow/compute/executor.go b/arrow/compute/executor.go
index c9e4c2ef..a804aae9 100644
--- a/arrow/compute/executor.go
+++ b/arrow/compute/executor.go
@@ -534,6 +534,13 @@ func (s *scalarExecutor) WrapResults(ctx context.Context,
out <-chan Datum, hasC
}
acc = nil
}
+ releaseOutput := func() {
+ if output != nil {
+ output.Release()
+ output = nil
+ }
+ releaseAccumulated()
+ }
toChunked := func() {
acc = output.(ArrayLikeDatum).Chunks()
@@ -562,15 +569,18 @@ func (s *scalarExecutor) WrapResults(ctx context.Context,
out <-chan Datum, hasC
for {
select {
case <-ctx.Done():
- // context is done, either cancelled or a timeout.
- // either way, we end early and return what we've got
so far.
- if output == nil {
- releaseAccumulated()
- return nil
- }
- return output
+ // A canceled call must not return a partial result.
+ releaseOutput()
+ return nil
case o, ok := <-out:
if !ok { // channel closed, wrap it up
+ // Cancellation and channel closure can become
ready together.
+ // Recheck the context so selecting the channel
cannot make a
+ // canceled call return a result
nondeterministically.
+ if ctx.Err() != nil {
+ releaseOutput()
+ return nil
+ }
if output != nil {
return output
}
@@ -1022,6 +1032,12 @@ func (v *vectorExecutor) WrapResults(ctx
context.Context, out <-chan Datum, hasC
case <-ctx.Done():
return nil
case output = <-out:
+ if output == nil || ctx.Err() != nil {
+ if output != nil {
+ output.Release()
+ }
+ return nil
+ }
}
// we got an output datum, but let's wait for the channel to
@@ -1031,6 +1047,10 @@ func (v *vectorExecutor) WrapResults(ctx
context.Context, out <-chan Datum, hasC
output.Release()
return nil
case <-out:
+ if ctx.Err() != nil {
+ output.Release()
+ return nil
+ }
return output
}
}
@@ -1046,6 +1066,13 @@ func (v *vectorExecutor) WrapResults(ctx
context.Context, out <-chan Datum, hasC
}
acc = nil
}
+ releaseOutput := func() {
+ if output != nil {
+ output.Release()
+ output = nil
+ }
+ releaseAccumulated()
+ }
toChunked := func() {
out := output.(ArrayLikeDatum).Chunks()
@@ -1089,15 +1116,18 @@ func (v *vectorExecutor) WrapResults(ctx
context.Context, out <-chan Datum, hasC
for {
select {
case <-ctx.Done():
- // context is done, either cancelled or a timeout.
- // either way, we end early and return what we've got
so far.
- if output == nil {
- releaseAccumulated()
- return nil
- }
- return output
+ // A canceled call must not return a partial result.
+ releaseOutput()
+ return nil
case o, ok := <-out:
if !ok { // channel closed, wrap it up
+ // Cancellation and channel closure can become
ready together.
+ // Recheck the context so selecting the channel
cannot make a
+ // canceled call return a result
nondeterministically.
+ if ctx.Err() != nil {
+ releaseOutput()
+ return nil
+ }
if output != nil {
return output
}
diff --git a/arrow/compute/executor_test.go b/arrow/compute/executor_test.go
index 1b7bfbe6..8de080c2 100644
--- a/arrow/compute/executor_test.go
+++ b/arrow/compute/executor_test.go
@@ -84,6 +84,33 @@ func
TestScalarExecutorWrapResultsReleasesAccumulatedOutputOnCancellation(t *tes
close(output)
}
+func TestScalarExecutorWrapResultsReleasesSingleOutputOnCancellation(t
*testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ builder := array.NewInt32Builder(mem)
+ builder.Append(42)
+ value := builder.NewInt32Array()
+ builder.Release()
+ defer value.Release()
+
+ output := make(chan Datum)
+ ctx, cancel := context.WithCancel(context.Background())
+ executor := &scalarExecutor{
+ nonAggExecImpl: nonAggExecImpl{outType: value.DataType()},
+ }
+
+ result := make(chan Datum, 1)
+ go func() {
+ result <- executor.WrapResults(ctx, output, false)
+ }()
+
+ output <- NewDatum(value)
+ cancel()
+ require.Nil(t, <-result)
+ close(output)
+}
+
func TestScalarExecutorWrapResultsHandlesClosedOutput(t *testing.T) {
output := make(chan Datum)
close(output)