dongjoon-hyun opened a new pull request, #59991:
URL: https://github.com/apache/spark/pull/59991

   ### What changes were proposed in this pull request?
   
   This PR fixes an Arrow memory leak when compressing a record batch fails, as 
a workaround until Arrow Java 20.0.0.
   
   It adds `ArrowCompressionUtils.createVectorUnloader` and uses it in all nine 
places that create a `VectorUnloader` with a codec: 
`ArrowConverters.ArrowBatchIterator`, `ArrowCachedBatchSerializer` (3), 
`PythonArrowInput` (2), `ColumnarArrowPythonInput` (2) and 
`CoGroupedArrowPythonRunner`.
   
   With a compressing codec, it returns a `ReleasingVectorUnloader`, which 
extends Arrow's `VectorUnloader` and wraps the codec in a 
`TrackingCompressionCodec`:
   - `TrackingCompressionCodec` keeps the buffer that the batch holds for each 
`compress` call: the input until the call returns, and the output after that.
   - When `getRecordBatch` throws, `ReleasingVectorUnloader` closes these 
buffers with `AutoCloseables.close`, which adds a failure of `close()` to the 
original exception as a suppressed exception. When it returns, the batch owns 
the buffers, so the list is only cleared.
   
   With `NoCompressionCodec`, which cannot fail, `createVectorUnloader` returns 
Arrow's own `VectorUnloader`, so the default configuration is unchanged.
   
   Arrow Java 20.0.0 fixes this in `VectorUnloader` and 
`AbstractCompressionCodec` (apache/arrow-java#1290). `ReleasingVectorUnloader` 
must be removed with that upgrade, because it would release the buffers twice. 
The new test then fails with `RefCnt has gone negative`, which I checked with a 
codec that closes its input on failure like the one in Arrow 20.
   
   Alternatives that were not taken:
   - Wrap only the codec in `createCompressionCodec`: a codec does not know 
where a batch ends, so it cannot tell the buffers of the failed batch from 
those of the batches already returned.
   - Copy `VectorUnloader.getRecordBatch` and `appendNodes` into Spark: it 
brings Arrow's traversal and buffer layout checks into Spark.
   
   ### Why are the changes needed?
   
   Arrow Java up to 19.0.0 does not release buffers when 
`CompressionCodec.compress` throws inside `VectorUnloader.getRecordBatch` 
(apache/arrow-java#1234):
   - `appendNodes` retains each buffer before passing it to `compress`, and 
`AbstractCompressionCodec.compress` closes its input only on success. So the 
buffer being compressed keeps the extra reference.
   - The buffers already compressed for the batch are only in a local list of 
`getRecordBatch`, which nothing releases.
   
   Closing the vectors and the allocator cannot release these buffers, so they 
stay allocated until the JVM exits.
   
   The realistic trigger is direct memory running out, because compression 
allocates an additional direct buffer per input buffer. Arrow's Netty allocator 
turns direct memory exhaustion into 
`org.apache.arrow.memory.OutOfMemoryException`, which is not fatal to the 
executor. So the executor keeps running with less direct memory for the next 
tasks.
   
   The task still fails with the original exception, because 
`TaskContext.runTaskWithListeners` only adds a failure of a completion listener 
as a suppressed exception. Closing the allocator reports the leak, as in this 
example with a capped allocator.
   
   ```
   org.apache.arrow.memory.OutOfMemoryException: Unable to allocate buffer of 
size 256 (rounded from 196) due to memory limit. Current allocation: 65536
        at 
org.apache.arrow.vector.compression.AbstractCompressionCodec.compress(AbstractCompressionCodec.java:41)
        at 
org.apache.arrow.vector.VectorUnloader.appendNodes(VectorUnloader.java:131)
        ...
        Suppressed: org.apache.spark.util.TaskCompletionListenerException: 
Memory was leaked by query. Memory leaked: (16384)
   ```
   
   On the zero-copy paths 
(`ArrowCachedBatchSerializer.convertArrowBatchZeroCopy` and 
`ColumnarArrowPythonInput.writeArrowDirect`), the buffers belong to the 
allocator of the upstream vectors, so the leak is reported by the owner of 
those vectors rather than by the code that failed.
   
   This only happens when `spark.sql.execution.arrow.compression.codec` is 
`zstd` or `lz4`; the default is `none`. By code reading, the affected paths 
have existed since Apache Spark 4.1.0 for `ArrowConverters`, `PythonArrowInput` 
and `CoGroupedArrowPythonRunner` (SPARK-54134, SPARK-54226), since Apache Spark 
4.2.0 for `ColumnarArrowPythonInput` (SPARK-56350), and since Apache Spark 
4.3.0 for `ArrowCachedBatchSerializer` (SPARK-57268).
   
   ### Does this PR introduce _any_ user-facing change?
   
   No.
   
   ### How was this patch tested?
   
   Pass the CIs with the newly added test case in `ArrowCompressionUtilsSuite`. 
Its codec fails while the second batch compresses its second buffer. The test 
checks that the first batch keeps its buffers and that closing the allocator 
finds no leak.
   
   I also ran the affected paths in tasks with a temporary suite, which is not 
part of this PR. It caps `ArrowUtils.rootAllocator` when the input rows run 
out, so that the first allocation inside `compress` fails, and then measures 
the memory left behind.
   
   | Path | Before | After |
   |---|---|---|
   | `ArrowBatchIterator` | 16384 bytes | 0 bytes |
   | `InternalRowToArrowCachedBatchIterator` | 16384 bytes | 0 bytes |
   | `ColumnarBatchToArrowCachedBatchIterator` zero-copy, in the upstream 
allocator | 8192 bytes | 0 bytes |
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Opus 5.5


-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to