Conversation
ffi_lib libraries loaded
|
Thank you for your investigation and the update! Wouldn't it make sense to enable |
b0b545c to
fbcafe2
Compare
@larskanis Yes, and I pulled that out into its own PR in #1199. Since it's been disabled for a long time I'd rather an explicit PR than bundling it here, but happy to merge them if you prefer! |
This seems like a bug in either the native code or in CRuby. Shouldn't these threads be shut down before the GC starts clearing referenced objects? |
Ideally, yes. I checked both CRuby 3.4.8 and 4.0.6: they stop Ruby Threads, run These aren't Ruby threads, though. They're pthreads created inside librdkafka, so CRuby can't see or stop them. So I agree the binding should clean up first, and we're fixing that path too. |
This sounds like an unlikely edge case/a bug in rdkafka-ruby, if it can happen. I'm negative on this, it's basically leaking memory to workaround missing necessary cleanup. |
|
BTW I wonder if dlclose() does anything when passed Which is unsafe if there is another dlopen() for that same library later, and will likely result in a harder-to-diagnose crash. |
|
If changing anything, I would simply not call |
|
I'm coming around to What if this PR only skipped That would also keep #1199 straightforward: enable |
Nice 👍
Is that necessary, for what?
One concern is there can be something very long in |
CRuby force-frees referenced `T_DATA` objects during VM finalization. `FFI::DynamicLibrary` normally responds by calling `dlclose`, which can unmap a library while its foreign native threads are still running. This has caused production shutdown crashes through librdkafka. Use a rooted sentinel's finalizer to mark the finalization phase before CRuby reaches forced `T_DATA` cleanup. Ordinary garbage collection and END handlers retain their existing unload behavior. Co-Authored-By: Pi <pi@shopify.com>
fbcafe2 to
9d37832
Compare
ffi_lib libraries loaded
Good point, I still think the ffi part is useful as a safety net. The binding should stop its own threads (karafka/rdkafka-ruby#964 does that for rdkafka) but if cleanup is incomplete, the forced sweep currently turns that into a SIGSEGV by unmapping code under the remaining threads. I added three child-process checks: ordinary GC still unloads, GC from an Does that work? |
FFI::DynamicLibrarycallsdlclosewhen garbage collection frees it. During VM finalization CRuby force-frees eligibleT_DATAobjects, including reachable library handles, so it can unmap code while foreign native threads are still executing it. This caused the Shopify worker shutdown crashes we've been chasing through librdkafka.I initially tried making
RTLD_NODELETEthe default, but that changed normal library lifetime and static-state behavior. This version leaves the existing flags and runtime unloading alone. ffi keeps a private rooted sentinel whose finalizer marks the finalization phase after END handlers and before forcedT_DATAcleanup;library_freeskipsdlcloseonly from that point onward.The regressions distinguish those phases: normal GC still unloads, an
at_exithandler registered before ffi still unloads, and a child with active native threads survives the forced shutdown sweep. Theat_exitcase fails with the earlierrb_set_end_procapproach.Bindings still own orderly cleanup—the corresponding rdkafka fix is karafka/rdkafka-ruby#964. This is the narrow safety net when that cleanup is incomplete or fails. An embedded host that destroys Ruby but keeps its process alive will retain mappings that reach forced finalization; standalone Ruby processes are about to return the whole address space to the OS.