Java: optional non-blocking finalizers via a native destruction queue#180
Open
amit-nayar wants to merge 2 commits into
Open
Java: optional non-blocking finalizers via a native destruction queue#180amit-nayar wants to merge 2 commits into
amit-nayar wants to merge 2 commits into
Conversation
A generated CppProxy.finalize() destroys the native object inline on the GC finalizer thread. If the C++ destructor can block — e.g. on a lock held by a long-running native operation — Android's FinalizerWatchdogDaemon kills the process once a single finalize() exceeds its deadline (10 s by default: 'TimeoutException: finalize() timed out'), and the blocked finalizer thread stalls every other pending finalization in the app. Add an opt-in flag, --java-non-blocking-finalizers: generated finalizers enqueue this::_djinni_private_destroy to a generated DjinniNativeDestructionQueue support class (emitted once per run), whose dedicated daemon thread may block safely. Enqueueing resurrects the proxy until destruction has run — legal per the JLS, an object's finalizer runs at most once — so the native reference stays valid; the existing AtomicBoolean guard is unchanged and explicit _djinni_private_destroy() callers keep synchronous destruction. Default is false: generated output is unchanged unless the flag is set. No JNI or C++/ObjC changes.
System.console() returns null when the test JVM runs without an interactive console (CI, piped output), so the integration-test suite aborted at construction with a NullPointerException whenever a previous run had left the result directory behind. Use println instead.
1a81686 to
722af07
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A generated
CppProxy.finalize()destroys the native object inline on the GC finalizer thread:If the C++ destructor can block — in our case a page object whose destructor acquires a rendering mutex that a long-running parse holds for tens of seconds — two things happen on Android:
FinalizerWatchdogDaemonenforces a deadline (10 s by default) perfinalize()and kills the process withjava.util.concurrent.TimeoutException: …CppProxy.finalize() timed out— a well-known fatal crash class in Play Console. We hit this in production-bound code.Change
New opt-in flag
--java-non-blocking-finalizers <true/false>(defaultfalse— generated output is byte-identical unless enabled). When enabled:Generated finalizers enqueue the destruction instead of destroying inline:
A
DjinniNativeDestructionQueuesupport class is emitted once per generation run (viacreateFileOnce) into the Java output package: an unbounded queue drained by a single daemon thread that may block freely; one failed destruction never kills the queue.Notes on correctness:
this::_djinni_private_destroydeliberately resurrects the proxy until the queue has destroyed it — legal per the JLS (an object's finalizer runs at most once) — so the native reference stays valid untilnativeDestroyhas run.AtomicBoolean destroyedguard is unchanged; explicit_djinni_private_destroy()callers keep synchronous, deterministic destruction.Cleaner/PhantomReference-based mechanism.Testing
non_blocking_finalizers.djinni+ expected output) covering the generated finalizer and the emitted support class.NullPointerExceptioninremoveTestOutputDirectory(System.console()isnullin non-interactive runs, e.g. CI/piped output), which aborted the integration suite whenever a previous run had left the result directory behind.We are running the equivalent patch in production at Nutrient (PSPDFKit) against our vendored 1.3.2 generator. Happy to adjust naming/approach to maintainer preference.