hcrosse opened a new issue, #1481:
URL: https://github.com/apache/arrow-go/issues/1481

   ### Describe the bug, including details regarding any error messages, 
version, and platform.
   
   Two paths in `arrow/compute` keep buffers referenced after the caller 
releases everything. When `ExecuteScalarExpression` fails to prepare its input, 
columns it already took stay referenced. Releasing a null dictionary scalar 
that the package created, such as `compute.NullLiteral` of a dictionary type, 
leaves its empty dictionary referenced.
   
   I reproduced both on main at `66722e30` on macOS arm64 with Go 1.26.10. With 
a synthetic record of one row, a failed cast of column `b` leaves 192 bytes 
allocated for column `a` after the input is released. Releasing 
`compute.NullLiteral` for `dictionary<int32, utf8>` leaves another 64 bytes. 
With v18.8.0 only the first leak reproduces.
   
   **Reproduction**
   
   Save this as `main.go` in a new subdirectory of an arrow-go checkout at 
`66722e30df98ddd6f7bb46023fa13287b84308f6`, then run `go run .` from that 
directory:
   
   ```go
   package main
   
   import (
        "context"
        "fmt"
        "strings"
   
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/compute"
        "github.com/apache/arrow-go/v18/arrow/compute/exprs"
        "github.com/apache/arrow-go/v18/arrow/memory"
   )
   
   func main() {
        // 1. A failed cast leaks the column added before it.
        mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
        schema := arrow.NewSchema([]arrow.Field{
                {Name: "a", Type: arrow.BinaryTypes.Binary, Nullable: true},
                {Name: "b", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
        }, nil)
        rec, _, _ := array.RecordFromJSON(mem, arrow.NewSchema([]arrow.Field{
                {Name: "a", Type: arrow.BinaryTypes.Binary, Nullable: true},
                {Name: "b", Type: arrow.BinaryTypes.String, Nullable: true},
        }, nil), strings.NewReader(`[{"a": "eA==", "b": "x"}]`))
   
        bldr := exprs.NewExprBuilder(exprs.NewDefaultExtensionSet())
        _ = bldr.SetInputSchema(schema)
        ex, _ := bldr.FieldRef("a").BuildExpr()
        _, err := 
exprs.ExecuteScalarExpression(compute.WithAllocator(context.Background(), mem), 
schema, ex, compute.NewDatumWithoutOwning(rec))
        rec.Release()
        fmt.Printf("cast error: %v\nbytes still allocated: %d\n", err, 
mem.CurrentAlloc())
   
        // 2. A null dictionary literal never releases its empty dictionary.
        dflt := memory.NewCheckedAllocator(memory.NewGoAllocator())
        memory.DefaultAllocator = dflt
        compute.NullLiteral(&arrow.DictionaryType{IndexType: 
arrow.PrimitiveTypes.Int32, ValueType: arrow.BinaryTypes.String}).Release()
        fmt.Printf("NullLiteral(dictionary) bytes still allocated: %d\n", 
dflt.CurrentAlloc())
   }
   ```
   
   On main:
   
   ```text
   cast error: invalid: strconv.ParseInt: parsing "x": invalid syntax
   bytes still allocated: 192
   NullLiteral(dictionary) bytes still allocated: 64
   ```
   
   **Expected behavior**
   
   The failed cast should return the same error, and both counts should be zero.
   
   **Cause**
   
   When a cast fails or a field name is ambiguous, `makeExecBatch` returns a 
new empty `ExecBatch`. That replaces its named result before the deferred 
cleanup reads it, so the values already in the batch are never released:
   
   
https://github.com/apache/arrow-go/blob/66722e30df98ddd6f7bb46023fa13287b84308f6/arrow/compute/exprs/exec.go#L40-L50
   
   
https://github.com/apache/arrow-go/blob/66722e30df98ddd6f7bb46023fa13287b84308f6/arrow/compute/exprs/exec.go#L66-L72
   
   https://github.com/apache/arrow-go/pull/1464 gave null dictionary scalars an 
empty dictionary and wrapped the new scalars in `NewDatumWithoutOwning`. 
https://github.com/apache/arrow-go/pull/1377 landed just before it and made 
`NewDatumWithoutOwning` borrow its value, with a `Release` that does nothing. 
Since both landed, releasing those datums no longer releases the scalars. The 
leak measurement in https://github.com/apache/arrow-go/pull/1464 was taken 
before https://github.com/apache/arrow-go/pull/1377 landed.
   
   
https://github.com/apache/arrow-go/blob/66722e30df98ddd6f7bb46023fa13287b84308f6/arrow/compute/expression.go#L602-L604
   
   The same wrapping is used for missing fields and null casts in 
`makeExecBatch`, and for Substrait null literals, which can have a dictionary 
type through a registered user defined type. The empty dictionary comes from 
`memory.DefaultAllocator`. The Go allocator reclaims it through garbage 
collection, so the leak shows with checked or C allocators. The failed cast 
leak holds the caller's own buffers whatever their allocator.
   
   I have a patch with tests and can open a PR.
   
   ### Component(s)
   
   Other
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to