Fix FFI close responsiveness and callback reclamation - #2622
Conversation
Run Node native connection close asynchronously, stabilize the Rust cleanup regression, and release Java callback wrapper roots after native quiescence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Node shutdown now resolves before asynchronous native cleanup finishes, changing its completion semantics.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
nodejs/src/ffiRuntimeHost.ts — Await asynchronous FFI cleanup before reporting shutdown complete |
What changed in this PR
Updates FFI shutdown behavior to keep Node responsive, stabilize Rust lifecycle testing, and reclaim Java callback wrappers.
Changes:
- Runs Node connection close asynchronously with expanded lifecycle coverage.
- Relaxes the Rust test’s scheduling-sensitive assertion.
- Releases Java callback roots after successful close.
| File | Description |
|---|---|
nodejs/src/ffiRuntimeHost.ts |
Moves native close to Koffi’s asynchronous API. |
nodejs/test/ffiRuntimeHost.test.ts |
Tests asynchronous close and resource retention. |
nodejs/test/e2e/inprocess_ffi.e2e.test.ts |
Verifies event-loop responsiveness during force-stop. |
rust/src/ffi.rs |
Makes the lifecycle assertion scheduling-independent. |
java/sdk/src/main/java/com/github/copilot/ffi/JnaNativeBinding.java |
Releases callback roots after quiescence. |
java/sdk/src/test/java/com/github/copilot/ffi/JnaNativeBindingTest.java |
Tests retention, reclamation, and shutdown ordering. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
I reviewed the relevant Node and Java discussions from #2610 for context. For Node, the five-second behavior was explicitly understood: the discussion explained that synchronous For Java, process-lifetime retention of the JNA callback wrapper was also explicit and intentional in #2610—the resolution stated that the wrapper root would remain intact after detaching its delegate. Given the clarified ABI guarantee that a successful Generated by Copilot |
stephentoub
left a comment
There was a problem hiding this comment.
The Java callback reclamation and Node async-close direction are sound in light of the #2610 discussions. Approving the overall change; please address the P2 shutdown-completion issue called out inline before merging.
Generated by Copilot
Restore successful-close completion in stop and forceStop without waiting through the existing detached retries after a non-quiescent close. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SDK Consistency Review — PR #2622This PR fixes an in-process FFI callback lifetime/quiescence bug and touches three SDKs (Java, Node.js, Rust). I compared the changed logic against the equivalent FFI host implementations in Go ( Findings: no cross-SDK inconsistency introduced ✅
VerdictThis is a targeted, language-specific bug fix confined to internal FFI plumbing (
|

Follow up on #2610 to fix three concrete defects: Node could block its event loop for five seconds during close with pending outbound traffic, the Rust lifecycle regression depended on thread scheduling, and Java permanently retained JNA callback wrappers after successful close.
connection_closethrough Koffi's async facility so outbound callbacks can finish on the event loop. Retain callback state and keepalive until close reports quiescence, then unregister the callback and shut down the host.Public shutdown completion/error contracts and the graceful-stop versus force-stop distinction are unchanged. No native ABI or cross-SDK cleanup redesign is included.
Validation
mvn verify, Spotless, and Checkstyle pass. The regression exercises real JNA trampoline upcalls, failed-close retention, successful-close garbage collection, and both shutdown orderings; it passed five repeated runs. Three optional packaged-runtime loading tests were skipped because the native JAR was unavailable.