Project

General

Profile

Actions

Bug #22216

open

Regexp backref and IO lastline are incompatible with Ractor

Bug #22216: Regexp backref and IO lastline are incompatible with Ractor

Added by headius (Charles Nutter) 1 day ago. Updated about 4 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:126187]

Description

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).

Updated by byroot (Jean Boussier) 1 day ago Actions #1 [ruby-core:126188]

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.

Updated by headius (Charles Nutter) about 12 hours ago · Edited Actions #2 [ruby-core:126191]

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 require access to the caller's frame.

$ 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.

Updated by byroot (Jean Boussier) about 11 hours ago Actions #3 [ruby-core:126192]

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.

Updated by headius (Charles Nutter) about 5 hours ago Actions #4 [ruby-core:126194]

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/RubyIO.java#L1897

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.

Updated by headius (Charles Nutter) about 5 hours ago Actions #5 [ruby-core:126195]

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.

Updated by headius (Charles Nutter) about 5 hours ago Actions #6 [ruby-core:126196]

ethically detectable

statically detectable

Updated by ko1 (Koichi Sasada) about 4 hours ago Actions #7 [ruby-core:126197]

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:

$~ = ...
# Conceptually compiled into:
binding.svar[current_thread].$~ = ...

(2) If we manage per-thread svars in each Thread, we need a dictionary for each Thread:

$~ = ...
# 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.

Actions

Also available in: PDF Atom