[ruby-core:126187] [Ruby Bug#22216] Regexp backref and IO lastline are incompatible with Ractor
Issue #22216 has been reported by headius (Charles Nutter). ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by byroot (Jean Boussier).
Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby.
I've always considered this a bug. If possible they should definitely be scoped to the current thread (or rather fiber?).
Because we cannot statically detect when they will be used
I would also love if we could statically know when they're used or not. One of my pet peeves is that `case str; when /foo/`, is noticeably slower because `Regexp#===` has to create a `MatchData` which is very often useless. Please correct me if I'm wrong, but the only thing that prevent such static analysis is `eval` right? I've had a bunch of discussion over the years about how we could take inspiration of JavaScript, and nerf `eval` if it is aliased, which would allow to statically detect eval usage and only deoptimize these methods. Such nerfed `eval` wouldn't have accept to `$` special variables, allowing to statically analyze whether they need to be computed and set in the first place. But there might be some considerations I'm missing here. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118264 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter).
I've always considered this a bug. If possible they should definitely be scoped to the current thread (or rather fiber?).
I prototyped this for JRuby some years ago, and I believe Rubinius (and perhaps TruffleRuby) implemented it this way. For JRuby we opted not to ship that behavior because it could be very visible, and there could be cases where someone expects to simply *read* a previously set `$~` or `$_`. We err on the side of matching CRuby. It's definitely doable, though.
Please correct me if I'm wrong, but the only thing that prevent such static analysis is eval right?
Well, that and the fact that you don't know if you're calling the `#[]=` method on a String or a Hash when you pass in a Regexp, or if the `gsub` you're calling is the core method that requires special frame storage. Statically analyzing offline is probably doable, but at runtime we don't have any guarantees of what method we're actually calling. JIT can do this at runtime, but then also needs to be able to deopt if someone throws a new type in.
nerf eval if it is aliased, which would allow to statically detect eval usage
JRuby takes a more conservative approach: we warn if you alias method names known to be associated with these special frame variables. ``` $ jruby -w -e 'class Foo; alias my_eval eval; end' -e:1: warning: Foo#eval accesses caller method's state and should not be aliased ``` If you do proceed with such an alias and then make calls to the aliased eval, you'll get unexpected results since we can no longer detect it's "THE eval". Nerfing it is an interesting strategy, though. We could implement that by replacing the aliased eval with the nerfed version, so it would still function but only have access to its own new frame. FWIW we do this for all methods that have such "special" behavior. ``` $ jruby -w -e 'class Foo < String; alias blah gsub; end' -e:1: warning: Foo#gsub accesses caller method's state and should not be aliased ``` They could be similarly "nerfed" as an alternative. Generally people don't alias these methods, because usually aliasing is done along with *wrapping*, which breaks access to the caller's frame anyway. But the problem still stands: you can't know just by looking at Ruby code (sans type hints or JIT profiles) whether a given method might access the caller's frame in "special" ways. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118267 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by byroot (Jean Boussier).
But the problem still stands: you can't know just by looking at Ruby code (sans type hints or JIT profiles) whether a given method might access the caller's frame in "special" ways.
I don't want to derail the discussion too much, but in short my idea was the opposite. At least in MRI, if we ignore `eval` it's easy to know if a method access these special variables (`getspecial` instruction), so we could have a call info flag that tells whether the caller access any sort of "special" variables. Methods like `Regexp#===` could then read the call info and skip updating these variables. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118268 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter).
it's easy to know if a method access these special variables (getspecial instruction),
That is not sufficient. There are methods that read *and* write these special variables without the user ever accessing the literal globals. You can look in the JRuby codebase to see where we have marked up core methods with metadata indicating they either read or write one of these variables. A few of them do both, in which case you must prepare that frame storage just in case. Example: https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/Ruby... Our list may not be exhaustive. And then there's Kernel#send and related calls which are also supposed to be able to access the callers frame, including these implicit variables. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118270 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter). Oh, it's also worth pointing out that there are Regexp method equivalents of each of the special backref variables (`last_match` etc). They are less commonly used, I believe, and usually called directly on Regexp, but that's another example that can access the implicit variable without being ethically detectable. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118271 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter).
ethically detectable
statically detectable ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118272 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by ko1 (Koichi Sasada). I agree that the current behavior is a Ractor bug. In fact, this is one of the blockers preventing us from removing the "experimental" warning from Ractor. These variables should be thread-local. The main implementation challenge is how to manage the lifetimes of Procs and Threads. I use the term “svar” (short for “special variables”), following CRuby's implementation terminology. (1) If we manage per-thread svars in each Proc or binding, we need a dictionary (or a list) for each Proc: ```ruby $~ = ... # Conceptually compiled into: binding.svar[current_thread].$~ = ... ``` (2) If we manage per-thread svars in each Thread, we need a dictionary for each Thread: ```ruby $~ = ... # Conceptually compiled into: Thread.current.svar[binding_or_proc].$~ = ... ``` In either case, we need to manage the lifetimes of the dictionary keys and values, which seems complicated. I am also not sure which approach would allow for a more efficient implementation. If we assume that this is a rare case, option (1) may be acceptable, and we could leave stale dictionary keys behind when threads terminate. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118273 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze). TruffleRuby already make these always thread-local, and there has been 0 compatibility issue reported about that. It's basically option (1) mentioned by @ko1, i.e. a dictionary/ThreadLocal object on the frame, but with the optimization that if only 1 Thread has written to it then we have direct field for the value: https://github.com/truffleruby/truffleruby/blob/33598a0447c4cd0b56eb56b80e81... (used [here](https://github.com/truffleruby/truffleruby/blob/33598a0447c4cd0b56eb56b80e81...)) ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118281 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze). To give a bit more details, TruffleRuby then always reserves one slot in the frame for svar's, and lazily allocate the storage for them on first access: https://github.com/truffleruby/truffleruby/blob/33598a0447c4cd0b56eb56b80e81... ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118282 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze).
in each Proc or binding
A small precision here which might be helpful is svars are only stored on method frames, never on block frames. So it's per method frame and Proc are unaffected (they just access the encapsulating method frame's svars). ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118284 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). I've wanted to fix this for a while. We should do what TruffleRuby does and others have described. Of the options ko1 proposed, I think with both these tables need to hold weak references (making them essentially the same thing) otherwise we risk leaking Thread objects (in option 1) or methods/environments (option 2). If they are weak references, it's probably easier to do option 2 since that won't require synchronization on the table. One question Jean posed that I don't see discussion about is whether this should be Thread-local or Fiber-local. My gut instinct was to prefer Fiber-local, but I think it will cause incompatibilities via `Enumerator.new` or ex. `gsub` with no block. ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118286 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze). jhawthorn (John Hawthorn) wrote in #note-11:
One question Jean posed that I don't see discussion about is whether this should be Thread-local or Fiber-local. My gut instinct was to prefer Fiber-local, but I think it will cause incompatibilities via `Enumerator.new` or ex. `gsub` with no block.
I think it should be Fiber-local, otherwise `$~` and friends might be set unexpectedly after a Fiber switch, especially with a Fiber scheduler which might transfer/resume/yield in non-obvious places. TruffleRuby uses a Fiber local (specifically a `java.lang.ThreadLocal` and since Fibers have their own Java Thread those are the same thing). ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118333 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). I think I'm _somewhat_ sold on it being Fiber-local. There may be some incompatibilities, but the existing behaviour is already strange. Here's the first case I was worried about. If you get an Enumerator with lazy or an iterating method without a block, $1 will change in the calling method. ``` $ ruby -ve 'g = "hello".gsub(/(.)/); g.next; p $1' ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25] "h" ``` ``` $ ruby -ve 'g = "hello".gsub(/(.)/); g.next; p $1' truffleruby 33.0.1 (2026-01-20), like ruby 3.3.7, Oracle GraalVM Native [arm64-darwin23] nil ``` However Ruby 4.0 behaves a bit strange with this once we move the test code into a method ``` $ ruby -ve 'def t; g = "hello".gsub(/(.)/); g.next; p $1; end; t' ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25] nil ``` However, I think this is a bug (one I spotted elsewhere through another path and had already intended on fixing). We can change the behaviour by making the block escape before making the enumerator (the implementation issue is that the ifunc's svar_lep is stale, pointing to the stack rather than the escaped env on the heap). ``` $ ruby -ve 'def t; proc{}; g = "hello".gsub(/(.)/); g.next; p $1; end; t' ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25] "h" ``` Since this is already inconsistent, that seems like an opportunity to make the switch to Fiber local? Here's a less broken case (avoids an ifunc so doesn't have the same issue). I could see someone actually writing this and shows a compatibility issue we might face: ```ruby def report access_log = [ %{127.0.0.1 - - [04/Aug/2026:10:00:00 -0700] "GET /index.html HTTP/1.1" 200 1043}, %{10.2.3.4 - - [04/Aug/2026:10:00:01 -0700] "POST /login HTTP/1.1" 302 0}, ] e = access_log.lazy.select { |line| line =~ /"(\w+) (\S+) HTTP/ } loop do e.next puts "#{$1} #{$2}" end end report ``` ``` $ ruby -v svar_access_log3.rb ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25] GET /index.html POST /login ``` ``` $ ruby -v svar_access_log3.rb truffleruby 33.0.1 (2026-01-20), like ruby 3.3.7, Oracle GraalVM Native [arm64-darwin23] ``` ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118341 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). I've started prototyping this in https://github.com/ruby/ruby/pull/18200 (needs testing and benchmarking still) ---------------------------------------- Bug #22216: Regexp backref and IO lastline are incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118342 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). Subject changed from Regexp backref and IO lastline are incompatible with Ractor to Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in same cases, incompatible with Ractor Just a note on the Ractor-incompatibility, I think that's actually a much smaller problem. We should only hit that via `Ractor.shareable_proc`, which is a point we can set svar as we want (or is a flag we could check for). We could fix this just for ractors easily, but I think we want to improve that for Threads (and _hopefully_ Fibers) at the same time so changing the title to reflect that. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in same cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118343 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). Here's an example I think folks would expect to be thread safe, but fails on current CRuby after just a few iterations (thanks to the Thread.pass) ``` ruby def test_it proc { |str| str =~ /(.)(.)(.)/ Thread.pass # Comment this out to make it pass (usually) ret = [$1, $2, $3] ret } end def assert_equal(a, b) raise "#{a.inspect} != #{b.inspect}" unless a == b end test = test_it() th = [] th << Thread.new do 1000.times { assert_equal test.call("abc"), %w[a b c] } end th << Thread.new do 1000.times { assert_equal test.call("123"), %w[1 2 3] } end th.each(&:join) ``` ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118345 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by matz (Yukihiro Matsumoto). I agree with the direction, and I agree these should be Fiber-local. What I am not ready to decide is the rule for cases like the one in [#note-13](https://bugs.ruby-lang.org/issues/22216#note-13). Reading `$1` after `e.next` is code people actually write, and I would rather not break it. I want a rule that keeps it working, not an exception list. One idea to consider: a fiber inherits its parent's storage, except non-blocking fibers, which get their own. Sibling fibers under a scheduler are where the visibility problem in [#note-12](https://bugs.ruby-lang.org/issues/22216#note-12) actually is. A plain `Fiber.new` resumed by its parent does not run concurrently with it, so sharing there is safe and matches today's behavior. What do you think of that? Also, please settle the stale `svar_lep` bug in [#note-13](https://bugs.ruby-lang.org/issues/22216#note-13) first. Until that is fixed we do not have a stable baseline to compare against. Matz. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118423 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze). matz (Yukihiro Matsumoto) wrote in #note-18:
Reading `$1` after `e.next` is code people actually write
Given that does not work on Ruby 4.0 (3rd snippet of #note-13) and there has been no bug report about that, it seems a strong indication people don't use such code. I think simplicity is best, inheriting storage between Fibers would add lots of complexity and make it much harder to understand the model semantically. That last part is not just to be nice, everyone has been confused for years with the existing semantics for `svars`, I think it's a great time to clean them up and just be Fiber=local + method-frame-local. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118437 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter). Popping back in to say I support the Fiber-isolated direction here but don't have strong opinions about the potential breaking cases. I've disliked supporting these variables for years so anything we can do to limit their scope is ok by me.
inheriting storage between Fibers would add lots of complexity
If this is not already a pattern for other Fiber state, I agree it would be unnecessary complexity. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118438 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by jhawthorn (John Hawthorn). matz (Yukihiro Matsumoto) wrote in #note-18:
What I am not ready to decide is the rule for cases like the one in [#note-13](https://bugs.ruby-lang.org/issues/22216#note-13). Reading `$1` after `e.next` is code people actually write, and I would rather not break it. I want a rule that keeps it working, not an exception list.
I'm not sure that it's code users write. I invented it as a demonstration. I however have been able to find code in the wild that's thread-unsafe.
[...] please settle the stale `svar_lep` bug in [#note-13](https://bugs.ruby-lang.org/issues/22216#note-13) first. Until that is fixed we do not have a stable baseline to compare against.
I will fix this. The point of bringing that up was to show that users already experience inconsistent results when combining threads/fibers and svars, which suggests this change is less likely to break existing code.
One idea to consider: a fiber inherits its parent's storage, except non-blocking fibers, which get their own. Sibling fibers under a scheduler are where the visibility problem in [#note-12](https://bugs.ruby-lang.org/issues/22216#note-12) actually is. A plain `Fiber.new` resumed by its parent does not run concurrently with it, so sharing there is safe and matches today's behaviour.
What do you think of that?
I don't know how to do this and worry that would end up more complicated for the user and implementation. We already have Fiber-local svars at the top level of a Fiber (regardless of blockingness), so that would either need to change or remain inconsistent. Having this dependent on blockingness is also confusing because it's a Fiber-scheduler concept. Fibers are non-blocking by default (`Fiber.new{}.blocking? == false`), so it's unclear what cases would benefit from inheritance. The Fiber scheduler can be registered and removed at any time, and fibers can transition from non-blocking to blocking in a block (and back when the block exits). I think enumerators are also non-blocking, so it's not likely this would fix the example I invented. I think making svars everywhere fiber-and-method-frame-local is the most consistent and safest. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118883 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by Eregon (Benoit Daloze). +1 to what John said. Making the semantics simpler has been wanted for a long time here, let's not add some complication with blocking Fibers or some inheritance of sort. I think no one writes code like `g = "hello".gsub(/(.)/); g.next; p $1`. I think `$~` should never cross stacks, when it does it's basically a bug (e.g. when crossing threads, or fibers with a scheduler). ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118897 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by matz (Yukihiro Matsumoto). You are right that the blocking based rule does not work, since fibers for enumerators are non-blocking. I withdraw that idea. I agree that plain Fiber-local should be the default. But I still think `e.next` is a real problem. The same block behaves differently with `e.each` and `e.next`, only because `next` uses a fiber internally. Both directions break: ```ruby e = log.lazy.select { |line| line =~ /"(\w+) (\S+) HTTP/ } e.next $1 # nil with Fiber-local svars "abc" =~ /(b)/ e = [1, 2].lazy.map { |x| "#{$1}#{x}" } e.next # "1" instead of "b1" ``` The `lazy.select` example in #note-13 works in Ruby 4.0. The observable behavior should not depend on whether the implementation uses a fiber. The difference is how the fiber is used. `e.next` is a synchronous call. The caller stops until the value comes back, so there is no concurrency. Sibling fibers under a scheduler are independent units, and that is where the problem in #note-12 is. We cannot tell them apart by `resume`, since schedulers may also use `resume`. So it should be decided when the fiber is created. My proposal: ```ruby Fiber.new(transparent: true) { ... } # name is tentative ``` * While resumed, the fiber shares the svars of frames outside its own stack (e.g. the method that owns the block) with the fiber that resumed it. Frames on its own stack have their own svars. * It cannot be transferred (`FiberError`). * Without the option, fibers are isolated (Fiber-local). Schedulers need no change. * `Enumerator` uses this option internally. Users who write their own generators with fibers can use it too. We already have `blocking:` and `storage:` to describe the relation between a fiber and its surroundings, so this fits there. For now it covers only svars, but the same issue exists for `Thread#[]` inside `e.next`, so the name should describe the role rather than svars. @jhawthorn, do you think this can be implemented on top of your PR? Matz. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-118935 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by ioquatix (Samuel Williams). @matz Fiber schedulers typically use `#transfer`, so preventing it would be problematic. There is also an assumption being made that it's desirable for `svars` to leak out of enumerators, which may not always be the case, e.g. ```ruby def process(section_header, lines) section_header =~ /^\[(\w+)\]$/ # outer match -> $1 == "database" section = $1 pairs = lines.lazy.map { |l| l =~ /(\w+)=(\w+)/; [$1, $2] } result = [] loop do k, v = pairs.next # fiber-backed pull result << "#{section}.#{k} = #{v}" result << " (still in section #{$1})" # author trusts $1 == "database" end result end ``` Therefore maybe it's better to go in the opposite direction: no matter what, enumerators should not leak `svars` to their outer scope? This does mean `e.next; $1` would return `nil`, but that pattern is already conceded to be non-idiomatic, and I think that's a better trade than making every enumerator capable of silently overwriting a caller's `$1`. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-119057 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter). ioquatix (Samuel Williams) wrote in #note-24:
@matz
Fiber schedulers typically use `#transfer`, so preventing it would be problematic.
There is also an assumption being made that it's desirable for `svars` to leak out of enumerators, which may not always be the case, e.g.
```ruby def process(section_header, lines) section_header =~ /^\[(\w+)\]$/ # outer match -> $1 == "database" section = $1
pairs = lines.lazy.map { |l| l =~ /(\w+)=(\w+)/; [$1, $2] }
result = [] loop do k, v = pairs.next # fiber-backed pull result << "#{section}.#{k} = #{v}" result << " (still in section #{$1})" # author trusts $1 == "database" end result end ```
Therefore maybe it's better to go in the opposite direction: no matter what, enumerators should not leak `svars` to their outer scope? This does mean `e.next; $1` would return `nil`, but that pattern is already conceded to be non-idiomatic, and I think that's a better trade than making every enumerator capable of silently overwriting a caller's `$1`.
ioquatix (Samuel Williams) wrote in #note-24:
@matz
Fiber schedulers typically use `#transfer`, so preventing it would be problematic.
There is also an assumption being made that it's desirable for `svars` to leak out of enumerators, which may not always be the case, e.g.
```ruby def process(section_header, lines) section_header =~ /^\[(\w+)\]$/ # outer match -> $1 == "database" section = $1
pairs = lines.lazy.map { |l| l =~ /(\w+)=(\w+)/; [$1, $2] }
result = [] loop do k, v = pairs.next # fiber-backed pull result << "#{section}.#{k} = #{v}" result << " (still in section #{$1})" # author trusts $1 == "database" end result end ```
Therefore maybe it's better to go in the opposite direction: no matter what, enumerators should not leak `svars` to their outer scope? This does mean `e.next; $1` would return `nil`, but that pattern is already conceded to be non-idiomatic, and I think that's a better trade than making every enumerator capable of silently overwriting a caller's `$1`.
It's a lovely idea but a very specific exception to introduce. These variables are specified to live in the nearest method frame, with no consideration for fiber locality. Lots of code expects them to be visible across block invocations, regardless of how those blocks are invoked. Switching your code to a fiber shouldn't suddenly make them disappear, or should it? Honestly, I don't have any answers other than eliminating these hidden variables altogether. You can't suddenly make them be block scoped because they won't be visible across block invocations. You can't suddenly make them fiber local, because all fibers are supposed to share a thread. And you can't make them truly global because that doesn't solve any problems and prevents nested calls from having their own versions. They simply are not compatible with immutable thread-safe scoping. Eliminating them is probably not something we are willing to do in a minor release, but it's probably the best choice long-term if safe concurrency is a future priority. I'm tired of telling people "don't do this" because it's embarrassing that there are core Ruby language features that are inherently concurrency-unsafe. I've been struggling with these things for 20 years, and they remain one of the biggest optimization and thread safety issues in JRuby today. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-119058 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by ioquatix (Samuel Williams). @headius Just thinking out loud...
Switching your code to a fiber shouldn't suddenly make them disappear, or should it?
I'd gently push back on this, because switching to a *thread* already does exactly that today, and has for 20 years: ```ruby def outer "abc" =~ /(b)/ before = $1 blk = proc { "xyz" =~ /(y)/ } # lexically owned by `outer` Thread.new { blk.call }.join # invoked on another thread [before, $1] end outer # => ["b", "b"] -- the block's match never reaches outer's $1 ``` So "these variables live in the nearest method frame, regardless of how the block is invoked" isn't quite the rule we have now. It's really *method-frame + thread*: the block shares `outer`'s frame lexically, but thread-locality overrides that. Execution context already changes svar visibility, and nobody considers the thread case a bug. If `Enumerator` used a `Thread` internally instead of a `Fiber`, we'd have the same isolation — so Fiber-local doesn't introduce a novel exception, it just makes fibers behave the way threads already do. To me that's *more* consistent, not less. It also seems like "method scope" isn't really a single well-defined thing here. A lazy enumerator is, by definition, evaluated somewhere other than where it's written, so its block's svar scope is genuinely ambiguous: ```ruby def process(section_header, pairs) section_header =~ /^\[(\w+)\]$/ # $1 in process's frame == "database" section = $1 result = [] loop do k, v = pairs.next # fiber-backed pull result << "#{section}.#{k} = #{v}" result << " (still in section #{$1})" # which $1 is this? end result end pairs = lines.lazy.map { |l| l =~ /(\w+)=(\w+)/; [$1, $2] } # block defined here process("[database]", pairs) p $1, $2 # => "port", "5432" (!) ``` What is the scope of `$1`/`$2` inside that block? Does it belong to where `pairs` is defined, or where `pairs.next` is called? The answer today is neither of the obvious ones: the block's matches land in its *lexical* definition site (the top-level frame), so after the call `$1`/`$2` at the top level are `"port"`/`"5432"` — the last match performed deep inside `process`'s loop, across a fiber boundary. Meanwhile `process`'s own `$1` stayed `"database"` the whole time. So the observable result depends on where the block was *written*, not where it ran, which most people wouldn't guess. And if that same block were driven by a `Thread` instead of a `Fiber`, the top-level `$1` wouldn't be clobbered at all — so this is a fiber-specific quirk, not a universal "svars live in the method frame" guarantee. The fiber itself carries a well-defined scope for the frames on its own stack; the only ambiguity is about frames it closed over that live *outside* its stack, and I don't think those are meaningfully "parented" to the consuming method at all. Like you, I don't know the single right answer here.
I don't have any answers other than eliminating these hidden variables altogether. [...] Eliminating them is probably the best choice long-term if safe concurrency is a future priority.
I basically agree — long term, removing them is the honest fix. But that's not something we can do without longer term planning, and in the meantime we still have to pick *some* behavior. My worry with `transparent:` is that it doubles down on "fibers don't isolate" precisely when threads already established the opposite precedent, and it does so to preserve `e.next; $1`, a pattern already conceded to be non-idiomatic. So I'd suggest plain Fiber-local (or EC local more precisely) as the near-term default: it's the simplest rule, it matches how threads already behave, and it doesn't require the non-transferable-fiber restriction (which conflicts with schedulers that rely on `#transfer`). ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-119069 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by headius (Charles Nutter).
I'd gently push back on this, because switching to a thread already does exactly that today, and has for 20 years:
This is not actually true. Your example works that way because a frame clones its block's immediate frame. If the frame is captured elsewhere as a proc, that proc will use the same frame for all threads. Most of the rest of your reply is moot once this frame is not the thread's block's immediate frame.
What is the scope of $1/$2 inside that block? Does it belong to where pairs is defined, or where pairs.next is called? The answer today is neither of the obvious ones: the block's matches land in its lexical definition site (the top-level frame)
Yup, that is exactly the problem. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-119075 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
Issue #22216 has been updated by ioquatix (Samuel Williams). @headius You're right - my bad — the isolation there is incidental, not a guarantee. If the frame is captured as a shared proc, threads race on it badly. But I think that actually restates my point rather than refuting it. There is no consistent "svars live in the method frame" rule to preserve today: - within a single method frame: fine; - shared across threads via a captured proc: shared *and* racy (#note-17); - across an enumerator's fiber: bound to the block's *lexical* definition site (which, as you agree, is exactly the problem); - under a fiber scheduler: different again. So there isn't a coherent status quo we'd be protecting. Given that, I'd rather not introduce a new `transparent:` API to bless the leaky path, especially if it needs the non-transferable-fiber restriction that breaks fiber schedulers. Plain EC-local (as in @jhawthorn's PR) seems like the best near-term default: it's the one rule that's simple to reason about, it's no worse than anything we have now, and it doesn't require any special-casing of fibers or a new fiber mode. If anything, it makes the concurrency story strictly less surprising than the current mix. ---------------------------------------- Bug #22216: Special variables (ex. Regexp backref and IO lastline) are thread-unsafe in some cases, incompatible with Ractor https://bugs.ruby-lang.org/issues/22216#change-119076 * Author: headius (Charles Nutter) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Problem Several Regexp-matching methods currently write (and sometimes read) the implicit "backref" `$~` variable in the local frame (and related variables like `$'`). Several IO methods read or write the "last line" `$_` variable in the same way. In both cases, the result is a mutable object, which makes these variables already problematic for ractors. Making matters worse, the frame might be shared if a proc is captured and used across threads or ractors, and there's no static way to inspect a piece of code to know if it expects to read or write these variables. Where procs can be rejected by a proc for accessing captured state, there's no such check possible for these variables. All of these facts make the backref and lastline variables fundamentally incompatible with Ractor. ## Possible remedies A wholesale removal of these variables would solve the problem, but there's a lot of code that depends on them... much of that code without even realizing it, since they might not access the variables directly. In some cases, the dependencies are internal and part of the behavior of core methods. Hard errors when using methods that read or write these variables would avoid introducing threading problems into a Ractor, but would also break a large number of commonly-used methods. There have been experiments to make these variables both frame and thread-local, but they have never been made standard. Updates to backref and lastline are visible across threads and already can lead to concurrency issues even on CRuby. Deprecating the implicit behavior and making it opt-in (or opt-out?), perhaps with keyword arguments or file pragmas, might be a halfway measure. It would probably not be an easy transition. I don't know the right path forward, but I believe this issue needs to be discussed. ## JRuby perspective We continue to mimic CRuby behavior, which has led to our users occasionally running into issues when a proc accesses these variables across threads. Our recommendation: "don't do that". We also have had our frustrations optimizing around these variables, since they implicitly require access across calls. Because we cannot statically detect when they will be used, we essentially treat all method names that might *potentially* access them as deopt triggers. It's not ideal. I'd like to hear ideas for how to make these variables less "magic", less implicit and easier to deal with across calls (and across threads/ractors). -- https://bugs.ruby-lang.org/
participants (7)
-
byroot (Jean Boussier) -
Eregon (Benoit Daloze) -
headius (Charles Nutter) -
ioquatix (Samuel Williams) -
jhawthorn (John Hawthorn) -
ko1 (Koichi Sasada) -
matz (Yukihiro Matsumoto)