From: "Eregon (Benoit Daloze) via ruby-core" Date: 2025-08-06T18:54:59+00:00 Subject: [ruby-core:122927] [Ruby Feature#21039] Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks Issue #21039 has been updated by Eregon (Benoit Daloze). I agree this should make `Ractor.shareable_proc` safe enough with a non-literal block and address the semantics issue in the OP. Cases with `eval` seem not possible to know and it seems rare enough to be OK to behave with "snapshot/copy of environment" semantics in that case. The rules are quite similar to the rules for the `Ractor.new {}` block, which could use the same rules (`Ractor.new {}` is currently stricter as it does not allow reading an environment variable, but it should/could for convenience & consistency). Of course there should still be a check that environment variables the block captures are shareable, which `Ractor.make_shareable(Proc)` already does: ```ruby nil.instance_exec { a = Object.new; Ractor.make_shareable(proc { a }) } # can not make shareable Proc because it can refer unshareable object # from variable 'a' (Ractor::IsolationError) ``` And same for the `self` around the block: ```ruby a = 1; Ractor.make_shareable(proc { a }) # Proc's self is not shareable: # (Ractor::IsolationError) ``` Regarding the reason for the 2nd example in @tenderlovemaking 's comment, it wouldn't cause race conditions / crashes because `shareable_proc` already makes a copy of the environment anyway (Aaron told me). But it would cause the semantics issue in the OP, that an assignment to outer variable is not observed and breaks Ruby block semantics, hence that case must be an exception from `Ractor.shareable_proc`, e.g. a `Ractor::IsolationError` or `ArgumentError`. ---------------------------------------- Feature #21039: Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks https://bugs.ruby-lang.org/issues/21039#change-114233 * Author: Eregon (Benoit Daloze) * Status: Assigned * 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 `: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/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/