Skip to content

fix(jni): keep Java owners alive during native calls - #111

Merged
ChristopherWirt merged 3 commits into
developfrom
cwirt/expression-reachability
Nov 10, 2026
Merged

ChristopherWirt merged 3 commits into
developfrom
cwirt/expression-reachability

Conversation

@ChristopherWirt

@ChristopherWirt ChristopherWirt commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #110.

Problem

  1. Java wrappers (Expression, Session, VortexWriter) own a native pointer. The Cleaner frees it once the wrapper is unreachable.
  2. The wrapper methods pass x.nativePointer(), a plain long, into JNI. Once that long is read, the JIT can treat x as dead.
  3. If a GC runs during the JNI call, the Cleaner frees the pointer while Rust is still reading it.
  4. Large Spark IN filters build thousands of temporary literals and pass them straight to literalList, which makes this likely. The result is an invalid expression or a SIGSEGV.

Fix

Wrap each JNI call that borrows a pointer in try { ... } finally { Reference.reachabilityFence(owner); }:

  • Expression factories: the operands, operand arrays, and dtype prototypes.
  • DataSource.scan: the projection and filter expressions. Before this change only this was fenced.
  • DataSource.open*, NativeFiles.*, Session.enableEdition / knownEditions, and VortexWriter create, writeBatch, and the metrics calls.
  • IndependentBuffersTest passed Session.create().nativePointer() from a temporary. It now keeps the session alive.

Regression test

ExpressionReachabilityTest starts a child JVM with a 32 MiB heap, a compiled literalList, and a background System.gc() loop. It builds 2,000 lists of 2,000 literals and checks the results of a real IN-filtered scan.

Verification

All runs used a freshly built debug libvortex_jni from this branch. The branch changes no Rust code.

Check Result
Regression test, original wrappers Fails 7/10 runs: must be a literal expression, got @… (a freed expression)
Regression test, this branch Passes 8/8 runs (about 10 s each)
Full :vortex-jni:test suite 164 tests, 0 failures
spotlessJavaCheck, git diff --check Pass

Not run: the production Spark workload, the vortex-spark tests, and CI-wide checks. The test is probabilistic, so on the original code it can still pass in some runs.

🤖 Generated with Claude Code

Signed-off-by: Christopher Wirt <cwirt@quantcast.com>
@ChristopherWirt ChristopherWirt self-assigned this Nov 10, 2026
ChristopherWirt and others added 2 commits November 10, 2026 09:56
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christopher Wirt <cwirt@quantcast.com>
Fence only the native call in VortexWriter.bytesWritten/bufferedBytes,
hoist DataSource.scan argument extraction out of the fenced block, and
drop no-op JVM flag and daemon setting from ExpressionReachabilityTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christopher Wirt <cwirt@quantcast.com>
@ChristopherWirt
ChristopherWirt merged commit aae4418 into develop Nov 10, 2026
1 check passed
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.

Expression factories let their arguments be freed during the native call (SIGSEGV in NativeExpression.literalList)

1 participant