Bug #21039
openRactor.make_shareable breaks block semantics (seeing updated captured variables) of existing blocks
Description
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
Updated by Eregon (Benoit Daloze) about 23 hours ago
- ruby -v set to ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [x86_64-linux]
Updated by Eregon (Benoit Daloze) about 23 hours ago
- Related to Bug #18243: Ractor.make_shareable does not freeze the receiver of a Proc but allows accessing ivars of it added
Updated by Eregon (Benoit Daloze) about 23 hours ago
- Related to Feature #21033: Allow lambdas that don't access `self` to be Ractor shareable added
Updated by luke-gru (Luke Gruber) about 13 hours ago
As far as I know this is intentional behavior, so even though I agree it is confusing I think this is more accurately a feature request instead of a bug.