
Issue #21039 has been updated by jhawthorn (John Hawthorn). Eregon (Benoit Daloze) wrote in #note-24:
```ruby counter = 0 get "/" do # assume the proc gets copied here so counter is 0 "Hello world #{counter}" counter += 1 end
counter += 123 ```
Assuming somewhere deep in Sinatra or Puma or so `Ractor.shareable_proc` is called with that block, I think it's a clear violation of the most basic semantics of Ruby blocks if that block doesn't see updates of `counter`.
Because of the `counter += 1` this falls into case 3 and raises an error. I agree case 3 is good and we should keep it, the proposed case 2 (forbidding the `counter += 123` mutation in the parent scope) is unhelpful and we should not implement it. I really do not believe it is a departure from the existing semantics. For the shareable proc, the environment is captured at the point `Ractor.shareable_proc` is called on a block, so the imagined DSL _could_ have called it at that time and seen those values. I think you are reaching with the imagined security/corruption concerns, all of which already exist with instance_eval, or just calling procs at different times (or concurrently in Threads, which is the specific situation this aims to replace). ko1 (Koichi Sasada) wrote in #note-26:
```ruby counter = 0 get "/", --> do # assume the proc gets copied here so counter is 0 "Hello world #{counter}" end
counter += 123 ```
I would prefer we didn't introduce a new syntax. Ractor adoption is quite challenging as-is, so having the ability to integrate into existing DSLs where it makes sense would be very helpful. ---------------------------------------- Feature #21039: Ractor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks https://bugs.ruby-lang.org/issues/21039#change-114298 * 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 `<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/