
Issue #21039 has been updated by Eregon (Benoit Daloze). Log at https://github.com/ruby/dev-meeting-log/blob/master/2025/DevMeeting-2025-08-... matz (Yukihiro Matsumoto) wrote in #note-28:
The only help to the original issue is to make `make_sharable(proc)` an error, so we can prohibit accidental make_sharable(proc). In that case, the sharable_proc() idea can be considered later. But in the different issue.
I think this would be good for now, until we find a good solution, to not have block semantics broken until then, and some code using Ractor to rely on the wrong semantics (even more broken given that `Ractor.make_sharable(proc)` mutates `proc` inplace). --- I think something we might all agree on for now is:
* Don't allow `Ractor.shareable_proc` blocks to use any variable from the environment (like current `Ractor.new`)
This is already used for `Ractor.new`. It's safe, clearly and simply defined, and more permissive than requiring a literal block. If the Proc accesses captured variables it's in most cases problematic anyway: * if the captured variables are reassigned, those assignments will be ignored silently (the main concern in this issue) * the value of these captures variables need to be shareable (otherwise it's an exception, and if it wasn't an exception it would be a segfault by leaking an unshareable object to another Ractor). This means anyway around where the block is defined you would often need some changes to make these variable values shareable (e.g. `.freeze` or `Ractor.make_sharable(...)`). In some cases it won't be possible, e.g. if the value is meant to be mutable. I think we should use concrete examples to make progress. @tenderlovemaking told me he had some places in Rails he wants to use this, could you link them here? Do they use captured variables? Is any of them assigned more than once? I think quite a few Sinatra apps wouldn't work, here are some examples: * If the Sinatra app or block-to-be-made-shareable uses state via local variables or constants, it can't work with Ractors (because `todos` must be mutable for the app to work): ```ruby todos = [] get '/' do ul { todos.map { li it } } end post '/add' do todos << params redirect '/' end ``` * If the Sinatra app or block-to-be-made-shareable uses captured variables or constants you would need to make the values of them shareable unless they are already deeply frozen, e.g.: ```ruby values = [1, 2, 3] # needs `.freeze` or `Ractor.make_sharable(...)` get '/' do values.sum end ``` * If the block-to-be-made-shareable uses singleton methods defined on the outer `self`, or `@ivar` of the outer self it won't work (that one is not an issue for Sinatra as Sinatra already changes the `self` of `get/post/...` blocks: ```ruby def self.answer 42 end @foo = 43 Ractor.shareble_proc(self: nil) do answer # NoMethodError due to having to change to a different `self` @foo # nil with Ractor due to having to change to a different `self` end ``` I think if you want to make Sinatra to work with Ractor, for most apps it's not possible to make the blocks shareable for these reasons and others (related to the general incompatibility of Ractor and state). I think a solution that would work much more often would be to combine Namespace and Ractor, and adapt Ractor semantics to be "if the Namespace in which this code was loaded is owned by the current Ractor" instead of "if main Ractor" (e.g. for whether it's allowed to assign a constant, or to accesses constants with a non-shareable value). So you'd load the code once per Ractor, and each Ractor would have a copy of the app and be able to run it, including necessary mutations. Though for that purpose sub-interpreters or forking might be superior in many ways. But still it would make Ractor a lot more usable, at the expense of having to load the code multiple times. ---------------------------------------- Feature #21039: Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks https://bugs.ruby-lang.org/issues/21039#change-114352 * Author: Eregon (Benoit Daloze) * Status: Closed * Assignee: ko1 (Koichi Sasada) ---------------------------------------- ```ruby def make_counter count = 0 nil.instance_exec do [-> { count }, -> { count += 1 }] end end get, increment = make_counter reader = Thread.new { sleep 0.01 loop do p get.call sleep 0.1 end } writer = Thread.new { loop do increment.call sleep 0.1 end } ractor_thread = Thread.new { sleep 1 Ractor.make_shareable(get) } sleep 2 ``` This prints: ``` 1 2 3 4 5 6 7 8 9 10 10 10 10 10 10 10 10 10 10 10 ``` But it should print 1..20, and indeed it does when commenting out the `Ractor.make_shareable(get)`. This shows a given block/Proc instance is concurrently broken by `Ractor.make_shareable`, IOW Ractor is breaking fundamental Ruby semantics of blocks and their captured/outer variables or "environment". It's expected that `Ractor.make_shareable` can `freeze` objects and that may cause some FrozenError, but here it's not a FrozenError, it's wrong/stale values being read. I think what should happen instead is that `Ractor.make_shareable` should create a new Proc and mutate that. However, if the Proc is inside some other object and not just directly the argument, that wouldn't work (like `Ractor.make_shareable([get])`). So I think one fix would to be to only accept Procs for `Ractor.make_shareable(obj, copy: true)`. FWIW that currently doesn't allow Procs, it gives `<internal:ractor>:828:in 'Ractor.make_shareable': allocator undefined for Proc (TypeError)`. It makes sense to use `copy` here since `make_shareable` effectively takes a copy/snapshot of the Proc's environment. I think the only other way, and I think it would be a far better way would be to not support making Procs shareable with `Ractor.make_shareable`. Instead it could be some new method like `isolated { ... }` or `Proc.isolated { ... }` or `Proc.snapshot_outer_variables { ... }` or so, only accepting a literal block (to avoid mutating/breaking an existing block), and that would snapshot outer variables (or require no outer variables like Ractor.new's block, or maybe even do `Ractor.make_shareable(copy: true)` on outer variables) and possibly also set `self` since that's anyway needed. That would make such blocks with different semantics explicit, which would fix the problem of breaking the intention of who wrote that block and whoever read that code, expecting normal Ruby block semantics, which includes seeing updated outer variables. Related: #21033 https://bugs.ruby-lang.org/issues/18243#note-5 Extracted from https://bugs.ruby-lang.org/issues/21033#note-14 -- https://bugs.ruby-lang.org/