Skip to content

fix(spark): release native read resources deterministically - #103

Merged
ChristopherWirt merged 3 commits into
developfrom
cwirt/read-native-resource-cleanup
Nov 9, 2026
Merged

ChristopherWirt merged 3 commits into
developfrom
cwirt/read-native-resource-cleanup

Conversation

@ChristopherWirt

@ChristopherWirt ChristopherWirt commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A Spark Vortex reader that finishes or closes leaves its native DataSource (and that source's file cache) alive until Java GC runs the Cleaner. Completed readers pile up native memory even when no Arrow batch is open.

Fixes #96.

Fix

  1. DataSource and Scan are now AutoCloseable. close() is idempotent. After it, calls throw IllegalStateException, except Scan.hasNext(), which returns false. Cleaner remains the fallback. Scans, partitions and batches already handed out stay valid.
  2. Scan.close() also frees a partition that hasNext() prefetched but next() never returned.
  3. VortexPartitionReader closes everything at EOF, on early close, and on failure: batch → Arrow reader → scan → source → readable. It attempts every close and keeps the original exception. Empty splits and footer reads follow the same rules.
  4. Partition.scanArrow is exception-safe. If stream allocation or Arrow import fails (e.g. a bounded allocator), the native partition or exported stream is released.

API changes

DataSource and Scan implement AutoCloseable, and their methods are synchronized so close() can't race a native call.

Results (local, dev native lib)

Before After
30 closed one-leaf readers, retained input buffers 225.3 MiB 0 (no GC)
Spark action input-buffer peak 120–198 MiB 4.1 MiB

Rows, checksums, allocation counts and warmed read times are unchanged. These are local measurements, not production OOM or throughput claims.

Testing

  • New ReadLifecycleTest (JNI) and VortexPartitionReaderLifecycleTest (Spark) cover: idempotent close, release without GC, primed-scan release, bounded-allocator failures, and an empty allocator after a reader closes.
  • Mutation checks: removing the source release or the pending-partition release makes the matching test fail. Both bounded-allocator cases fail with the old stream handoff.
  • 72 tests pass: JNI ReadLifecycleTest/TestMinimal/NativeIOBridgeTest, plus reader-lifecycle and footer tests on Spark 4.0 and Spark 3.5 (Scala 2.12 and 2.13), plus Spark 4.0 count tests.

Upstream

Upstream develop still frees source/scan handles only through the Cleaner. No equivalent upstream issue or PR exists. vortex-data#8075 (session lifetime) and vortex-data#11123/vortex-data#11124/vortex-data#11127/vortex-data#11197/vortex-data#11198 (decode/output buffering) are related but separate.

🤖 Generated with Claude Code

Close data sources and scans explicitly across reader and footer lifecycles. Release prefetched partitions and make Arrow stream handoff failures exception-safe, with native-ownership regression coverage.

Fixes #96

Signed-off-by: Christopher Wirt <[email protected]>
ChristopherWirt and others added 2 commits November 9, 2026 16:53
- Flatten Partition.scanArrow failure handling.
- Drop the unused currentPartition field from VortexPartitionReader.
- Test that a closed Arrow reader leaves its allocator empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Christopher Wirt <[email protected]>
Signed-off-by: Christopher Wirt <[email protected]>
(cherry picked from commit bfbc5bb)
Signed-off-by: Christopher Wirt <[email protected]>
@ChristopherWirt
ChristopherWirt force-pushed the cwirt/read-native-resource-cleanup branch from 0d24b2e to 15dad52 Compare November 9, 2026 16:53
@ChristopherWirt
ChristopherWirt merged commit f7dd84e into develop Nov 9, 2026
1 check passed
ChristopherWirt added a commit that referenced this pull request Nov 10, 2026
## Summary

Fix the shared Linux musl builder failure on `develop` and PRs #103,
#105, #106, #107, and #108 after the upstream lazy-constructor update.

## Changes

Normalize lists rebuilt from list views through the same `reset_offsets`
path as ordinary lists before forwarding their children to layout
writers. Rebuilding a sliced list view can now leave a lazy elements
slice; a flat child writer cannot serialize that expression. This
preserves the existing referenced-element bounds and resolves the slice
without forcing unrelated encodings into canonical arrays.

Keep the existing nullable sliced-list-view regression and add the
non-nullable case.

## CI Failure Analysis

### Status

Pre-existing base-branch regression, independent of the individual PR
changes. The same failure appears on [develop run
42649358621](https://github.com/quantcast/vortex/actions/runs/42649358621)
and every completed failing PR builder inspected (#105–#108).

### Failed Jobs

- `Rust tests (linux-musl)`: test failure in
`layouts::list::reader::tests::sliced_list_chunks_round_trip::case_4_list_view_chunks`.

### Relevant Log Output

```text
Error: Array vortex.slice is not registered for serialization
```

### Correlation With PR Changes

The upstream merge makes array slicing lazy. The list-view branch of
`canonicalize_to_list_parts` forwarded a rebuilt list's lazy elements
directly, while the ordinary-list branch already normalized them. The
Spark PRs do not change this writer; #107 changes separate zone-pruning
code.

### Recommended Next Step

Merge this shared fix into `develop`. The same patch is being applied to
the affected open PR branches so their new builders include the fix
immediately.

## Validation

- Reproduced the exact failing test on unmodified current `develop`
(exit 100, same serialization error).
- `cargo nextest run -p vortex-layout -E 'test(layouts::list::)'`: all
161 tests passed after the fix, including the original failing case and
new non-nullable case.
- `cargo clippy -p vortex-layout --all-targets --all-features -- -D
warnings`: passed.
- Pinned nightly Rust formatting and `git diff --check`: passed.

Local validation ran on macOS; the fresh Linux musl builders will verify
the runner-specific job.

Signed-off-by: Christopher Wirt <[email protected]>
robert3005 pushed a commit to vortex-data/vortex that referenced this pull request Nov 11, 2026
## Summary

Fixes #11364.

Closing a Spark reader currently leaves its Java-owned native read
handles and file caches alive until Cleaner processing. Make that
release explicit and idempotent, while preserving the resources still
needed by derived scans, returned partitions and Arrow batches. Ports
the lifecycle fix from [QC
#103](quantcast#103).

## Changes

- Make `DataSource` and `Scan` implement `AutoCloseable`, retaining
Cleaner fallback. Keep native calls synchronized with explicit close;
fence scan options so their expressions remain alive across JNI.
- Release partitions prefetched by `hasNext()` but never returned.
- Let Spark's owning wrappers close at EOF. Reader and footer cleanup
attempts every close in ownership order and preserves the original
failure.
- Release native partition ownership when Arrow stream allocation or
import fails.
- Reject reads on closed handles with `IllegalStateException`. An open,
exhausted scan still returns `false` from `hasNext()` and throws
`NoSuchElementException` from `next()`.

---------

Signed-off-by: Christopher Wirt <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spark reader close retains native file caches until Java GC

2 participants