From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-06-12T11:15:56+00:00 Subject: [ruby-core:125725] [Ruby Bug#22098] RUBY_INTERNAL_THREAD_EVENT_RESUMED runs without GVL held Issue #22098 has been updated by Eregon (Benoit Daloze). One reason it would be valuable to be able to allocate in RESUMED is that's a good spot to set the per-thread state. I looked at the datadog gem, gvltools, gvl-tracing and vernier. All of them except vernier use `rb_internal_thread_specific_get()/rb_internal_thread_specific_set()`, but also all of them need to mark some things inside the per-thread state (to mark a sample taken in a signal handler, etc). * vernier uses a `std::vector` to keep all per-thread states and mark them (that probably means linear lookup with the # of thread then + need to lock around it so some contention). * until recently, datadog used a `st_table` to keep all per-thread states and mark them (+ an integer in `rb_internal_thread_specific`, as it couldn't touch the `st_table` in GVL events). * datadog master uses a TypedData object stored as a hidden ivar on the Thread, this is simpler and convenient, it always has the full per-thread state with just `rb_internal_thread_specific_get` (no need for a `st_table` and no need to remove dead threads) * gvltools and gvl-tracing use `rb_internal_thread_specific` and a TypedData object stored as a Fiber-local (`rb_thread_local_aset`) on the Thread Storing as a Fiber-local/Thread-local/ivar on the Thread is not ideal because that would fail if the Thread is frozen (but I think nobody freezes Threads). Maybe `rb_internal_thread_specific_*()` should support GC-marking the value to fix that, but that still wouldn't help regarding allocations. A good place to allocate that TypedData object would be in RESUMED, that's both the initial state of a thread, and a good state to start tracking at for threads that existed before the profiler. The approach in https://github.com/Shopify/gvltools/pull/34 is to iterate the threads when starting the profiler, and to use the `RUBY_INTERNAL_THREAD_EVENT_STARTED` event, but that's not guaranteed to be called with the GVL according to the docs at least (and unclear if guaranteed to be called on the new Thread, if not could be problematic if the thread runs for a very short time and we try to cleanup the state concurrently (in EXITED) with initializing it). So we're saying in this issue the event that tracks getting the GVL may not have the GVL but the STARTED event which might not have the GVL is OK to rely on having the GVL? (I'm just highlighting how surprising that may be, but I think it may be fine if well documented and if it holds on existing releases) The current approach in the datadog gem is to allocate the thread states in a postponed job after the periodic signal handler sample, where it also samples other threads. Any RESUMED event before that sample is effectively lost, because we can't allocate in RESUMED, allocating in a postponed job after RESUMED is too late since we have nowhere to store the data. We could iterate threads when the profiler starts, but that wouldn't capture new threads (and unclear if `RUBY_INTERNAL_THREAD_EVENT_STARTED` can allocate). Maybe a regular :thread_start TracePoint should be used? That has guarantees about the GVL and running on the right thread, but is heavier. BTW because we can't call Ruby APIs in the RESUMED event, we need to use a postponed job. The means the postponed job must be scheduled quickly after the RESUMED event, as notably if there would be another GVL event in between that would cause problems. ---------------------------------------- Bug #22098: RUBY_INTERNAL_THREAD_EVENT_RESUMED runs without GVL held https://bugs.ruby-lang.org/issues/22098#change-117598 * Author: luke-gru (Luke Gruber) * Status: Open * Assignee: luke-gru (Luke Gruber) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- Today, it's possible to get a deadlock when allocating during a hook for this event. I attached a reproduction script using the `gvltools` gem. One way to fix it would be to not allocate during this hook and change the documentation to be clear that the GVL is not held. Any gems relying on this behavior, like `gvltools`, would need to be patched. Another way to approach it would be to change the call site for this hook invocation. I believe it would need to be added to quite a few places. To maintain Ractor safety, it would also need to be invoked without any locks held. I'm curious about your thoughts @byroot. ---Files-------------------------------- repro.rb (3.11 KB) run_loop.sh (2.82 KB) -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/