Skip to content

fix: load the bundled native library only once per class loader - #6493

Merged
andygrove merged 5 commits into
apache:mainfrom
zhangfengcdt:fix/native-library-single-load
Oct 2, 2026
Merged

andygrove merged 5 commits into
apache:mainfrom
zhangfengcdt:fix/native-library-single-load

Conversation

@zhangfengcdt

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #6096.

Rationale for this change

The cause is that the native library gets loaded more than once. CometSparkSessionExtensionsSuite resets NativeBase's loaded flag and calls load() again. Each call unpacks the bundled library to a new, randomly named temporary file and loads it.

To the JVM and the OS loader that is a separate library with its own statics, so the process ends up with several copies and only the first has JAVA_VM set.

JDK 17 resolves a JNI method on first call by iterating a hash map of loaded libraries, so a method first called after that suite can bind to an uninitialized copy. Which copy wins depends on the random file names, which is why the failure is intermittent. setShufflePartitionPusher is first called in CometNativeShuffleSuite, which is why the abort lands there.

What changes are included in this PR?

  • NativeBase: a flag that is never reset records that the bundled library has been loaded, so a repeat load() skips unpacking and loading another copy. The rest of the load sequence is unchanged.
  • CometSparkSessionExtensionsSuite: a regression test that resets the flag, reloads, and asserts no new library file was unpacked.

How are these changes tested?

The new regression test fails without the fix and passes with it.

I also reproduced the issue by running CometSparkSessionExtensionsSuite followed by CometNativeShuffleSuite in one JVM, on macOS with Spark 4.1.

  • Without the fix: 4 of 5 runs failed with JAVA_VM not initialized, starting at the test right after the callback registration test, as in the issue.
  • With the fix: 9 of 9 runs passed, with exactly one library copy unpacked.

Reloading unpacked the library to a new temporary file, which the JVM
loads as a second copy with uninitialized native state. JNI methods
could then bind to that copy and fail with JAVA_VM not initialized.

Closes apache#6096
@github-actions github-actions Bot added the bug Something isn't working label Oct 1, 2026

@andygrove andygrove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production fix matches the diagnosed failure mechanism, and the Linux exec job completed 132 suites in one JVM. I found one test-isolation issue that should be addressed before merge.

// The bundled library is unpacked to a new temporary file each time it is loaded. To the JVM
// a second copy is a distinct library with its own uninitialized native state, and a JNI
// method first called afterwards can bind to it (#6096).
def unpackedLibraries(): Set[String] =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this assertion process-local? It snapshots every libcomet-* file in java.io.tmpdir, so another Comet test JVM or parallel worktree that starts during this window will appear in added even though this class loader did not reload anything. A child JVM with an isolated temporary directory, or a test-visible load counter or path owned by NativeBase, would preserve the regression coverage without depending on unrelated processes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! It should be fixed now. It now compares the path of the library before and after a reload and asserts it is the same object. It no longer reads the temp directory at all.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Prior state and problem: Resetting NativeBase.loaded allowed repeated extraction and loading of bundled libraries, creating separate native state and inconsistent JNI bindings.
  • Design approach: Track successful bundled-library loading independently with bundledLibraryLoaded.
  • Correctness / compatibility analysis: The existing synchronized load() protects the new flag, which is set after System.load succeeds. Logging initialization, timezone checks, Arrow properties, and java.library.path loading remain intact. Reviewed native initialization, OpenJDK 17 library resolution, and Spark class-loader sources for 3.4.3, 3.5.9, 4.0.4, 4.1.3, and 4.2.0.
  • Key design decisions: The flag has the defining class loader’s lifetime. One boolean addresses the lifecycle distinction without additional abstraction or query-path work.
  • Implementation sketch: The production change guards bundled extraction. The regression test resets loaded, reloads, and compares temporary-library filenames.
  • Behavioral changes worth calling out: A JDK 17 harness produced one bundled library across four initialization calls at the reviewed head, versus four libraries at the base. Arrow-property handling, independent class loaders, and library-path loading passed.
  • Suggested improvements: The existing test-isolation concern remains a substantiated P2 blocker. A controlled second JVM sharing java.io.tmpdir caused the assertion to fail despite the first JVM correctly retaining its original library. Maven normally isolates temporary directories by checkout, but the assertion still measures unrelated JVMs sharing that directory. Use an owned library path/count or an isolated child-JVM directory, as already requested.

No additional introduced P1/P2 issues found within this review. The existing concern is not duplicated as a new finding.

Reviewed the entire diff from 62ed9d16d547fed1ac25260a1910893f076ab638 to 5b276e961eda9b4e914e4870bfe4174d32889ea1, including both changed files and surrounding code. The PR remains open and non-draft. Routed skills: review-comet-pr and review-comet-ffi-pr.

Exact-head CI: Comet CI remains in progress. Preflight, change detection, general lint, Scala syntactic lint, and CodeQL passed. Linux builds, Rust tests, Java lint, and compatibility checks remain in progress. macOS, Spark SQL, and Iceberg suites were skipped. No completed failures were reported.

Validation limits: The disposable harness compiled unmodified head/base NativeBase.java with dependency stubs and a minimal JNI library, using a Java equivalent of the regression assertion. Reproduce with python /tmp/comet-6493-review-validation/validate.py. Full Comet/Spark suites and macOS execution were not run. The checkout had no built artifacts and remains unchanged.

Compare the library path NativeBase recorded instead of listing
java.io.tmpdir, which other Comet JVMs also write to.
@zhangfengcdt
zhangfengcdt requested a review from andygrove October 1, 2026 04:46

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Prior state and problem: Resetting NativeBase.loaded allowed repeated bundled-library extraction, creating separate native state and inconsistent JNI bindings.
  • Design approach: Retain the successfully loaded library in bundledLibrary, independently of the resettable flag.
  • Correctness / compatibility analysis: The existing synchronization protects the retained reference, which is assigned after System.load succeeds. Reinitialization, Arrow properties, and java.library.path loading remain intact. Compared OpenJDK 17 library resolution and Spark class-loader setup for supported versions 3.4.3, 3.5.9, 4.0.4, 4.1.3, and 4.2.0. No SQL semantics change.
  • Key design decisions: One retained File per defining class loader captures the required lifetime without a new abstraction or query-path overhead.
  • Implementation sketch: Skip bundled extraction when that reference exists. The regression test resets loaded, reloads, and checks reference identity.
  • Behavioral changes worth calling out: Focused validation retained one library across four initialization calls, versus four at the base. Independent class loaders, library-path loading, and Arrow-property handling passed. The revised assertion also passed while another JVM loaded a library into the shared temporary directory, addressing the existing test-isolation concern despite its thread remaining open.
  • Suggested improvements: None at P1/P2. No introduced P1/P2 issues found within this review.

Reviewed the entire two-file diff from 9c42391cb9a6fe38d87f59992dd5fb4260483dbb to 0d4d9a3f5f4a0808fb90ec5bac7564892f9bb0b9, surrounding code, and all supplied discussions. The PR is non-draft. Routed skills: review-comet-pr and review-comet-ffi-pr.

Exact-head CI: Comet CI succeeded. All 24 completed checks passed, with 15 skipped. The Linux Spark 4.1/JDK 17 execution job passed 1,178 tests, including the new regression and Arrow-property tests. macOS, Spark SQL, and Iceberg suites were skipped.

Validation limits: Local validation compiled exact head/base NativeBase.java with dependency stubs and a minimal JNI library on JDK 17. The regression failed as expected at the base and with the head's guard removed. This validates loader behavior, not full native execution. Full Comet/Spark suites and macOS execution were not run locally. Project files remain unchanged.

@andygrove andygrove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest head. The previous test-isolation concern is addressed, and I found no additional issues.

@andygrove
andygrove added this pull request to the merge queue Oct 1, 2026
@andygrove
andygrove removed this pull request from the merge queue due to a manual request Oct 1, 2026
@andygrove
andygrove added this pull request to the merge queue Oct 1, 2026
Merged via the queue into apache:main with commit 4b7e29d Oct 2, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

native: JVMClasses::with_env: JAVA_VM not initialized aborts a multi-suite JVM

3 participants