Fix silent callback loss on JRuby (parser hangs after JVM GC) - #39
Merged
Conversation
JRuby's FFI does not retain Ruby references to procs assigned into FFI::Struct callback fields. The procs assigned into LLHttp::Callbacks were referenced nowhere else, so on JRuby they became garbage as soon as Parser#initialize returned. Once the JVM collected them, their native trampolines were reclaimed and llhttp_execute would complete successfully without invoking any callbacks, silently producing an unparsed result. Keep an explicit reference to each callback proc on the parser instance so the trampolines live exactly as long as the parser that owns them. MRI is unaffected (its FFI already retains struct-assigned procs).
Owner
|
@sferik fix makes sense, thanks for tracking this down! I'll cut a new release soon. |
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.
On JRuby,
LLHttp::Parserintermittently stops invoking delegate callbacks after a JVM garbage-collection cycle:llhttp_executereturns success, but no callbacks fire and the parser reports no progress. Downstream, this manifests as consumers blocking forever waiting for parse results on data they have already received. This turned out to be the root cause of an intermittent hang in httprb/http’s test suite on JRuby.Parser#initializeassigns each callback proc directly into theLLHttp::CallbacksFFI struct and keeps no other reference to it:MRI’s FFI retains a Ruby reference for procs assigned into
FFI::Structcallback fields, but JRuby’s FFI (JNR) does not. On JRuby the procs are therefore unreachable as soon asinitializereturns; when the JVM collects them, their native trampolines are reclaimed while the llhttp settings struct still points at them. Subsequentllhttp_executecalls complete without error and without invoking any callbacks.Here is a minimal reproduction:
On jruby 10.1.0.0 with llhttp-ffi 0.5.1 (macOS arm64) this fails on iteration 0. Without the explicit
System.gcit still fails, just less deterministically. In a real HTTP client workload, I measured roughly one silent parse failure per 25–70 parsers under normal GC pressure.The patch I’m proposing keeps a reference to each assigned proc on the parser instance, tying the trampolines’ lifetime to the parser that owns them. Please let me know if there are any changes you’d like me to make.