mirror of
https://github.com/ruby/ruby.git
synced 2026-11-08 01:20:26 +08:00
Instead of rb_proc_isolate (which rejected any outer variable), dup the handler and make the copy shareable exactly as Ractor.shareable_proc and Ractor.new now do. With no remaining callers, remove rb_proc_isolate, rb_proc_isolate_bang and the isolate-only code paths in vm.c. Method procs passed to Ractor.new now raise RuntimeError (not supported yet), like Ractor.shareable_proc; they previously ran unisolated with their receiver.
3429 lines
72 KiB
Ruby
3429 lines
72 KiB
Ruby
# Ractor.current returns a current ractor
|
|
assert_equal 'Ractor', %q{
|
|
Ractor.current.class
|
|
}
|
|
|
|
# Ractor.new returns new Ractor
|
|
assert_equal 'Ractor', %q{
|
|
Ractor.new{}.class
|
|
}
|
|
|
|
# Ractor.allocate is not supported
|
|
assert_equal "[:ok, :ok]", %q{
|
|
rs = []
|
|
begin
|
|
Ractor.allocate
|
|
rescue => e
|
|
rs << :ok if e.message == 'allocator undefined for Ractor'
|
|
end
|
|
|
|
begin
|
|
Ractor.new{}.dup
|
|
rescue
|
|
rs << :ok if e.message == 'allocator undefined for Ractor'
|
|
end
|
|
|
|
rs
|
|
}
|
|
|
|
# A Ractor can have a name
|
|
assert_equal 'test-name', %q{
|
|
r = Ractor.new name: 'test-name' do
|
|
end
|
|
r.name
|
|
}
|
|
|
|
# If Ractor doesn't have a name, Ractor#name returns nil.
|
|
assert_equal 'nil', %q{
|
|
r = Ractor.new do
|
|
end
|
|
r.name.inspect
|
|
}
|
|
|
|
# Raises exceptions if initialize with an invalid name
|
|
assert_equal 'ok', %q{
|
|
begin
|
|
r = Ractor.new(name: [{}]) {}
|
|
rescue TypeError => e
|
|
'ok'
|
|
end
|
|
}
|
|
|
|
# Ractor.new must call with a block
|
|
assert_equal "must be called with a block", %q{
|
|
begin
|
|
Ractor.new
|
|
rescue ArgumentError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# Ractor#inspect
|
|
# Return only id and status for main ractor
|
|
assert_equal "#<Ractor:#1 running>", %q{
|
|
Ractor.current.inspect
|
|
}
|
|
|
|
# Return id, loc, and status for no-name ractor
|
|
assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{
|
|
r = Ractor.new { '' }
|
|
r.join
|
|
sleep 0.1 until r.inspect =~ /terminated/
|
|
r.inspect
|
|
}
|
|
|
|
# Return id, name, loc, and status for named ractor
|
|
assert_match /^#<Ractor:#([^ ]*?) Test Ractor .+:[0-9]+ terminated>$/, %q{
|
|
r = Ractor.new(name: 'Test Ractor') { '' }
|
|
r.join
|
|
sleep 0.1 until r.inspect =~ /terminated/
|
|
r.inspect
|
|
}
|
|
|
|
# A return value of a Ractor block will be a message from the Ractor.
|
|
assert_equal 'ok', %q{
|
|
# join
|
|
r = Ractor.new do
|
|
'ok'
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# Passed arguments to Ractor.new will be a block parameter
|
|
# The values are passed with Ractor-communication pass.
|
|
assert_equal 'ok', %q{
|
|
# ping-pong with arg
|
|
r = Ractor.new 'ok' do |msg|
|
|
msg
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# Pass multiple arguments to Ractor.new
|
|
assert_equal 'ok', %q{
|
|
# ping-pong with two args
|
|
r = Ractor.new 'ping', 'pong' do |msg, msg2|
|
|
[msg, msg2]
|
|
end
|
|
'ok' if r.value == ['ping', 'pong']
|
|
}
|
|
|
|
# Ractor#send passes an object with copy to a Ractor
|
|
# and Ractor.receive in the Ractor block can receive the passed value.
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
msg = Ractor.receive
|
|
end
|
|
r.send 'ok'
|
|
r.value
|
|
}
|
|
|
|
# Ractor#receive_if can filter the message
|
|
assert_equal '[1, 2, 3]', %q{
|
|
ports = 3.times.map{Ractor::Port.new}
|
|
|
|
r = Ractor.new ports do |ports|
|
|
ports[0] << 3
|
|
ports[1] << 1
|
|
ports[2] << 2
|
|
end
|
|
a = []
|
|
a << ports[1].receive # 1
|
|
a << ports[2].receive # 2
|
|
a << ports[0].receive # 3
|
|
a
|
|
}
|
|
|
|
# dtoa race condition
|
|
assert_equal '[:ok, :ok, :ok]', %q{
|
|
n = 3
|
|
n.times.map{
|
|
Ractor.new{
|
|
10_000.times{ rand.to_s }
|
|
:ok
|
|
}
|
|
}.map(&:value)
|
|
}
|
|
|
|
assert_equal "42", %q{
|
|
a = 42
|
|
Ractor.shareable_lambda{ a }.call
|
|
}
|
|
|
|
# Ractor.shareable_proc issue for locals in proc [Bug #18023]
|
|
assert_equal '[:a, :b, :c, :d, :e]', %q{
|
|
v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e
|
|
closure = Proc.new { [v1, v2, v3, v4, v5] }
|
|
Ractor.shareable_proc(&closure).call
|
|
}
|
|
|
|
# Ractor.shareable_proc makes a copy of given Proc
|
|
assert_equal '[true, true]', %q{
|
|
pr1 = Proc.new do
|
|
self
|
|
end
|
|
pr2 = Ractor.shareable_proc(&pr1)
|
|
|
|
[pr1.call == self, pr2.call == nil]
|
|
}
|
|
|
|
# Ractor.shareable_proc keeps the original Proc intact
|
|
assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{
|
|
a = 42
|
|
pr1 = Proc.new do
|
|
[self.class, eval("a"), binding.local_variable_get(:a)]
|
|
end
|
|
a += 1
|
|
pr2 = Ractor.shareable_proc(&pr1)
|
|
|
|
r = []
|
|
begin
|
|
pr2.call
|
|
rescue SyntaxError
|
|
r << SyntaxError
|
|
end
|
|
|
|
r << pr1.call << pr1.binding.class
|
|
}
|
|
|
|
# Ractor.make_shareable mutates the original Proc
|
|
# This is the current behavior, it's currently considered safe enough
|
|
# because in most cases it would raise anyway due to not-shared self or not-shared captured variable value
|
|
assert_equal '[[42, 42], Binding, true, SyntaxError, "Can\'t create Binding from isolated Proc"]', %q{
|
|
a = 42
|
|
pr1 = nil.instance_exec do
|
|
Proc.new do
|
|
[eval("a"), binding.local_variable_get(:a)]
|
|
end
|
|
end
|
|
|
|
r = [pr1.call, pr1.binding.class]
|
|
|
|
pr2 = Ractor.make_shareable(pr1)
|
|
r << pr1.equal?(pr2)
|
|
|
|
begin
|
|
pr1.call
|
|
rescue SyntaxError
|
|
r << SyntaxError
|
|
end
|
|
|
|
begin
|
|
r << pr1.binding
|
|
rescue ArgumentError
|
|
r << $!.message
|
|
end
|
|
|
|
r
|
|
}
|
|
|
|
# Ractor::IsolationError cases
|
|
assert_equal '3', %q{
|
|
ok = 0
|
|
|
|
begin
|
|
a = 1
|
|
Ractor.shareable_proc{a}
|
|
a = 2
|
|
rescue Ractor::IsolationError => e
|
|
ok += 1
|
|
end
|
|
|
|
begin
|
|
cond = false
|
|
b = 1
|
|
b = 2 if cond
|
|
Ractor.shareable_proc{b}
|
|
rescue Ractor::IsolationError => e
|
|
ok += 1
|
|
end
|
|
|
|
begin
|
|
1.times{|i|
|
|
i = 2
|
|
Ractor.shareable_proc{i}
|
|
}
|
|
rescue Ractor::IsolationError => e
|
|
ok += 1
|
|
end
|
|
}
|
|
|
|
###
|
|
###
|
|
# Ractor still has several memory corruption so skip huge number of tests
|
|
if ENV['GITHUB_WORKFLOW'] == 'Compilations'
|
|
# ignore the follow
|
|
else
|
|
|
|
# Ractor.select with a Ractor argument
|
|
assert_equal 'ok', %q{
|
|
# select 1
|
|
r1 = Ractor.new{'r1'}
|
|
port, obj = Ractor.select(r1)
|
|
if port == r1 and obj == 'r1'
|
|
'ok'
|
|
else
|
|
# failed
|
|
[port, obj].inspect
|
|
end
|
|
}
|
|
|
|
# Ractor.select from two ractors.
|
|
assert_equal '["r1", "r2"]', %q{
|
|
# select 2
|
|
p1 = Ractor::Port.new
|
|
p2 = Ractor::Port.new
|
|
r1 = Ractor.new(p1){|p1| p1 << 'r1'}
|
|
r2 = Ractor.new(p2){|p2| p2 << 'r2'}
|
|
ps = [p1, p2]
|
|
as = []
|
|
port, obj = Ractor.select(*ps)
|
|
ps.delete(port)
|
|
as << obj
|
|
port, obj = Ractor.select(*ps)
|
|
as << obj
|
|
as.sort #=> ["r1", "r2"]
|
|
}
|
|
|
|
# Ractor.select from multiple ractors.
|
|
assert_equal 30.times.map { 'ok' }.to_s, %q{
|
|
def test n
|
|
rs = (1..n).map do |i|
|
|
Ractor.new(i) do |i|
|
|
"r#{i}"
|
|
end
|
|
end
|
|
as = []
|
|
all_rs = rs.dup
|
|
|
|
n.times{
|
|
r, obj = Ractor.select(*rs)
|
|
as << [r, obj]
|
|
rs.delete(r)
|
|
}
|
|
|
|
if as.map{|r, o| r.object_id}.sort == all_rs.map{|r| r.object_id}.sort &&
|
|
as.map{|r, o| o}.sort == (1..n).map{|i| "r#{i}"}.sort
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
end
|
|
|
|
30.times.map{|i|
|
|
test i
|
|
}
|
|
} unless (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878
|
|
|
|
# Exception for empty select
|
|
assert_match /specify at least one Ractor::Port or Ractor/, %q{
|
|
begin
|
|
Ractor.select
|
|
rescue ArgumentError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to send into a terminated ractor
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
end
|
|
|
|
r.join # closed
|
|
sleep 0.1 until r.inspect =~ /terminated/
|
|
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Can mix with Thread#interrupt and Ractor#join [Bug #17366]
|
|
assert_equal 'err', %q{
|
|
Ractor.new do
|
|
t = Thread.current
|
|
begin
|
|
Thread.new{ t.raise "err" }.join
|
|
rescue => e
|
|
e.message
|
|
end
|
|
end.value
|
|
}
|
|
|
|
# Killed Ractor's thread yields nil
|
|
assert_equal 'nil', %q{
|
|
Ractor.new{
|
|
t = Thread.current
|
|
Thread.new{ t.kill }.join
|
|
}.value.inspect #=> nil
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to send into a ractor with closed default port
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new {
|
|
Ractor.current.close
|
|
Ractor.main << :ok
|
|
Ractor.receive
|
|
}
|
|
|
|
Ractor.receive # wait for ok
|
|
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Ractor.main returns main ractor
|
|
assert_equal 'true', %q{
|
|
Ractor.new{
|
|
Ractor.main
|
|
}.value == Ractor.current
|
|
}
|
|
|
|
# a ractor with closed outgoing port should terminate
|
|
assert_equal 'ok', %q{
|
|
Ractor.new do
|
|
Ractor.current.close
|
|
end
|
|
|
|
true until Ractor.count == 1
|
|
:ok
|
|
}
|
|
|
|
# an exception in a Ractor main thread will be re-raised at Ractor#receive
|
|
assert_equal '[RuntimeError, "ok", true]', %q{
|
|
r = Ractor.new do
|
|
raise 'ok' # exception will be transferred receiver
|
|
end
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
[e.cause.class, #=> RuntimeError
|
|
e.cause.message, #=> 'ok'
|
|
e.ractor == r] #=> true
|
|
end
|
|
}
|
|
|
|
# an exception in a Ractor will be re-raised at Ractor#value
|
|
assert_equal '[RuntimeError, "ok", true]', %q{
|
|
r = Ractor.new do
|
|
raise 'ok' # exception will be transferred receiver
|
|
end
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
[e.cause.class, #=> RuntimeError
|
|
e.cause.message, #=> 'ok'
|
|
e.ractor == r] #=> true
|
|
end
|
|
}
|
|
|
|
# an exception in a Ractor non-main thread will not be re-raised at Ractor#receive
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
Thread.new do
|
|
raise 'ng'
|
|
end
|
|
sleep 0.1
|
|
'ok'
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# SystemExit from a Ractor is re-raised
|
|
# [Bug #21505]
|
|
assert_equal '[SystemExit, "exit", true]', %q{
|
|
r = Ractor.new { exit }
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
[e.cause.class, #=> RuntimeError
|
|
e.cause.message, #=> 'ok'
|
|
e.ractor == r] #=> true
|
|
end
|
|
}
|
|
|
|
# SystemExit from a Thread inside a Ractor is re-raised
|
|
# [Bug #21505]
|
|
assert_equal '[SystemExit, "exit", true]', %q{
|
|
r = Ractor.new { Thread.new { exit }.join }
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
[e.cause.class, #=> RuntimeError
|
|
e.cause.message, #=> 'ok'
|
|
e.ractor == r] #=> true
|
|
end
|
|
}
|
|
|
|
# threads in a ractor will killed
|
|
assert_equal '{ok: 3}', %q{
|
|
Ractor.new Ractor.current do |main|
|
|
q = Thread::Queue.new
|
|
Thread.new do
|
|
q << true
|
|
loop{}
|
|
ensure
|
|
main << :ok
|
|
end
|
|
|
|
Thread.new do
|
|
q << true
|
|
while true
|
|
end
|
|
ensure
|
|
main << :ok
|
|
end
|
|
|
|
Thread.new do
|
|
q << true
|
|
sleep 1
|
|
ensure
|
|
main << :ok
|
|
end
|
|
|
|
# wait for the start of all threads
|
|
3.times{q.pop}
|
|
end
|
|
|
|
3.times.map{Ractor.receive}.tally
|
|
} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec()
|
|
|
|
# unshareable object are copied
|
|
assert_equal 'false', %q{
|
|
obj = 'str'.dup
|
|
r = Ractor.new obj do |msg|
|
|
msg.object_id
|
|
end
|
|
|
|
obj.object_id == r.value
|
|
}
|
|
|
|
# Copying an object uses the native copier or Marshal#dump; it never calls the
|
|
# user-visible #clone.
|
|
assert_match /can not copy Thread object/, %q{
|
|
obj = Thread.new{}
|
|
begin
|
|
r = Ractor.new obj do |msg|
|
|
msg
|
|
end
|
|
rescue Ractor::Error => e
|
|
e.message
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# send shareable and unshareable objects
|
|
assert_equal "ok", <<~'RUBY', frozen_string_literal: false
|
|
port = Ractor::Port.new
|
|
echo_ractor = Ractor.new port do |port|
|
|
loop do
|
|
v = Ractor.receive
|
|
port << v
|
|
end
|
|
end
|
|
|
|
class C; end
|
|
module M; end
|
|
S = Struct.new(:a, :b, :c, :d)
|
|
|
|
shareable_objects = [
|
|
true,
|
|
false,
|
|
nil,
|
|
1,
|
|
1.1, # Float
|
|
1+2r, # Rational
|
|
3+4i, # Complex
|
|
2**128, # Bignum
|
|
:sym, # Symbol
|
|
'xyzzy'.to_sym, # dynamic symbol
|
|
'frozen'.freeze, # frozen String
|
|
/regexp/, # regexp literal
|
|
/reg{true}exp/.freeze, # frozen dregexp
|
|
[1, 2].freeze, # frozen Array which only refers to shareable
|
|
{a: 1}.freeze, # frozen Hash which only refers to shareable
|
|
[{a: 1}.freeze, 'str'.freeze].freeze, # nested frozen container
|
|
S.new(1, 2).freeze, # frozen Struct
|
|
S.new(1, 2, 3, 4).freeze, # frozen Struct
|
|
(1..2), # Range on Struct
|
|
(1..), # Range on Struct
|
|
(..1), # Range on Struct
|
|
C, # class
|
|
M, # module
|
|
Ractor.current, # Ractor
|
|
]
|
|
|
|
unshareable_objects = [
|
|
'mutable str'.dup,
|
|
[:array],
|
|
{hash: true},
|
|
S.new(1, 2),
|
|
S.new(1, 2, 3, 4),
|
|
S.new("a", 2).freeze, # frozen, but refers to an unshareable object
|
|
]
|
|
|
|
results = []
|
|
|
|
shareable_objects.map{|o|
|
|
echo_ractor << o
|
|
o2 = port.receive
|
|
results << "#{o} is copied" unless o.object_id == o2.object_id
|
|
}
|
|
|
|
unshareable_objects.map{|o|
|
|
echo_ractor << o
|
|
o2 = port.receive
|
|
results << "#{o.inspect} is not copied" if o.object_id == o2.object_id
|
|
}
|
|
|
|
if results.empty?
|
|
:ok
|
|
else
|
|
results.inspect
|
|
end
|
|
RUBY
|
|
|
|
# frozen Objects are shareable
|
|
assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
def initialize freeze
|
|
@a = 1
|
|
@b = :sym
|
|
@c = 'frozen_str'
|
|
@c.freeze if freeze
|
|
@d = true
|
|
end
|
|
end
|
|
|
|
def check obj1
|
|
obj2 = Ractor.new obj1 do |obj|
|
|
obj
|
|
end.value
|
|
|
|
obj1.object_id == obj2.object_id
|
|
end
|
|
|
|
results = []
|
|
results << check(C.new(true)) # false
|
|
results << check(C.new(true).freeze) # true
|
|
results << check(C.new(false).freeze) # false
|
|
RUBY
|
|
|
|
# move example2: String
|
|
# touching moved object causes an error
|
|
assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false
|
|
# move
|
|
r = Ractor.new do
|
|
obj = Ractor.receive
|
|
obj << ' world'
|
|
end
|
|
|
|
str = 'hello'
|
|
r.send str, move: true
|
|
modified = r.value
|
|
|
|
begin
|
|
str << ' exception' # raise Ractor::MovedError
|
|
rescue Ractor::MovedError
|
|
modified #=> 'hello world'
|
|
else
|
|
raise 'unreachable'
|
|
end
|
|
RUBY
|
|
|
|
# move example2: Array
|
|
assert_equal '[0, 1]', %q{
|
|
r = Ractor.new do
|
|
ary = Ractor.receive
|
|
ary << 1
|
|
end
|
|
|
|
a1 = [0]
|
|
r.send a1, move: true
|
|
a2 = r.value
|
|
begin
|
|
a1 << 2 # raise Ractor::MovedError
|
|
rescue Ractor::MovedError
|
|
a2.inspect
|
|
end
|
|
}
|
|
|
|
# move preserves aliasing inside the moved object graph
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
Ractor.receive
|
|
end
|
|
|
|
leaf = +"leaf"
|
|
moved = r.send([leaf, leaf], move: true).value
|
|
moved[0].equal?(moved[1])
|
|
}
|
|
|
|
# move handles cyclic references safely and preserves aliasing
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
Ractor.receive
|
|
end
|
|
|
|
a = []
|
|
a << a
|
|
moved = r.send(a, move: true).value
|
|
moved.equal?(moved[0])
|
|
}
|
|
|
|
# unshareable frozen objects should still be frozen in new ractor after move
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
obj = receive
|
|
{ frozen: obj.frozen? }
|
|
end
|
|
obj = [Object.new].freeze
|
|
r.send(obj, move: true)
|
|
r.value[:frozen]
|
|
}
|
|
|
|
# Access to global-variables are prohibited (read)
|
|
assert_equal 'can not access global variable $gv from non-main Ractor', %q{
|
|
$gv = 1
|
|
r = Ractor.new do
|
|
$gv
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Access to global-variables are prohibited (write)
|
|
assert_equal 'can not access global variable $gv from non-main Ractor', %q{
|
|
r = Ractor.new do
|
|
$gv = 1
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# $stdin,out,err is Ractor local, but shared fds
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
[$stdin, $stdout, $stderr].map{|io|
|
|
[io.object_id, io.fileno]
|
|
}
|
|
end
|
|
|
|
[$stdin, $stdout, $stderr].zip(r.value){|io, (oid, fno)|
|
|
raise "should not be different object" if io.object_id == oid
|
|
raise "fd should be same" unless io.fileno == fno
|
|
}
|
|
'ok'
|
|
}
|
|
|
|
# $stdin,out,err belong to Ractor
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
$stdin.itself
|
|
$stdout.itself
|
|
$stderr.itself
|
|
'ok'
|
|
end
|
|
|
|
r.value
|
|
}
|
|
|
|
# $DEBUG, $VERBOSE are Ractor local
|
|
assert_equal 'true', %q{
|
|
$DEBUG = true
|
|
$VERBOSE = true
|
|
|
|
def ractor_local_globals
|
|
/a(b)(c)d/ =~ 'abcd' # for $~
|
|
`echo foo` unless /solaris/ =~ RUBY_PLATFORM
|
|
|
|
{
|
|
# ractor-local (derived from created ractor): debug
|
|
'$DEBUG' => $DEBUG,
|
|
'$-d' => $-d,
|
|
|
|
# ractor-local (derived from created ractor): verbose
|
|
'$VERBOSE' => $VERBOSE,
|
|
'$-w' => $-w,
|
|
'$-W' => $-W,
|
|
'$-v' => $-v,
|
|
|
|
# process-local (readonly): other commandline parameters
|
|
'$-p' => $-p,
|
|
'$-l' => $-l,
|
|
'$-a' => $-a,
|
|
|
|
# process-local (readonly): getpid
|
|
'$$' => $$,
|
|
|
|
# thread local: process result
|
|
'$?' => $?,
|
|
|
|
# scope local: match
|
|
'$~' => $~.inspect,
|
|
'$&' => $&,
|
|
'$`' => $`,
|
|
'$\'' => $',
|
|
'$+' => $+,
|
|
'$1' => $1,
|
|
|
|
# scope local: last line
|
|
'$_' => $_,
|
|
|
|
# scope local: last backtrace
|
|
'$@' => $@,
|
|
'$!' => $!,
|
|
|
|
# ractor local: stdin, out, err
|
|
'$stdin' => $stdin.inspect,
|
|
'$stdout' => $stdout.inspect,
|
|
'$stderr' => $stderr.inspect,
|
|
}
|
|
end
|
|
|
|
h = Ractor.new do
|
|
ractor_local_globals
|
|
end.value
|
|
ractor_local_globals == h #=> true
|
|
}
|
|
|
|
# selfs are different objects
|
|
assert_equal 'false', %q{
|
|
r = Ractor.new do
|
|
self.object_id
|
|
end
|
|
ret = r.value
|
|
ret == self.object_id
|
|
}
|
|
|
|
# self is a Ractor instance
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
self.object_id
|
|
end
|
|
ret = r.value
|
|
if r.object_id == ret #=> true
|
|
true
|
|
else
|
|
raise [ret, r.object_id].inspect
|
|
end
|
|
}
|
|
|
|
# given block Proc is copied like Ractor.shareable_proc: it can read outer variables
|
|
# which are shareable and never reassigned
|
|
assert_equal '[42, "ok"]', %q{
|
|
a = 42
|
|
b = "ok".freeze
|
|
r = Ractor.new do
|
|
[a, b]
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# ... through nested blocks and with arguments
|
|
assert_equal '[4, 8]', %q{
|
|
a = 1
|
|
r = Ractor.new(2) do |x|
|
|
v = [1].map { |y| a + x + y }.first
|
|
[v, v * 2]
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# an outer variable which is reassigned can not be accessed
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
begin
|
|
a = true
|
|
r = Ractor.new do
|
|
a
|
|
end
|
|
a = false
|
|
rescue => e
|
|
e.class
|
|
end
|
|
}
|
|
|
|
# an outer variable holding an unshareable object can not be accessed
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
begin
|
|
a = []
|
|
r = Ractor.new do
|
|
a
|
|
end
|
|
rescue => e
|
|
e.class
|
|
end
|
|
}
|
|
|
|
# an outer variable can not be written
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
begin
|
|
a = 1
|
|
Ractor.new do
|
|
a = 2
|
|
end
|
|
rescue => e
|
|
e.class
|
|
end
|
|
}
|
|
|
|
# Ractor.new does not mutate the given Proc
|
|
assert_equal '[false, false, 1, 2]', %q{
|
|
pr = Proc.new { 1 }
|
|
r1 = Ractor.new(&pr)
|
|
r2 = Ractor.new(&pr)
|
|
pr.instance_variable_set(:@foo, Object.new)
|
|
[Ractor.shareable?(pr), pr.frozen?, r1.value, r2.value + 1]
|
|
}
|
|
|
|
# eval with outer locals in a Ractor raises SyntaxError
|
|
# [Bug #21522]
|
|
assert_equal 'SyntaxError', %q{
|
|
outer = 42
|
|
r = Ractor.new do
|
|
eval("outer")
|
|
end
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.class
|
|
end
|
|
}
|
|
|
|
# eval of an undefined name in a Ractor raises NameError
|
|
assert_equal 'NameError', %q{
|
|
r = Ractor.new do
|
|
eval("totally_undefined_name")
|
|
end
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.class
|
|
end
|
|
}
|
|
|
|
# eval of a local defined inside the Ractor works
|
|
assert_equal '99', %q{
|
|
Ractor.new { inner = 99; eval("inner").to_s }.value
|
|
}
|
|
|
|
# ivar in shareable-objects are not allowed to access from non-owner Ractor
|
|
assert_equal "can not get unshareable values from instance variables of classes/modules created by another Ractor (@iv from C)", <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
@iv = 'str'
|
|
end
|
|
|
|
r = Ractor.new do
|
|
class C
|
|
p @iv
|
|
end
|
|
end
|
|
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# setting an ivar of a class/module from a non-owner Ractor reports the ivar and class
|
|
assert_equal "can not set instance variables of classes/modules created by another Ractor (@iv from C)", <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
@iv = 'str'
|
|
end
|
|
|
|
r = Ractor.new do
|
|
class C
|
|
@iv = 'other'
|
|
end
|
|
end
|
|
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# a non-main Ractor can read, set, and remove ivars of classes/modules it created
|
|
assert_equal 'ok', <<~'RUBY', frozen_string_literal: false
|
|
Ractor.new do
|
|
[Class.new, Module.new].each do |mod|
|
|
mod.instance_variable_set(:@iv, 'str')
|
|
raise unless mod.instance_variable_get(:@iv) == 'str'
|
|
raise unless mod.remove_instance_variable(:@iv) == 'str'
|
|
end
|
|
:ok
|
|
end.value
|
|
RUBY
|
|
|
|
# even main cannot modify another Ractor's module; reporting its name must not call to_s
|
|
assert_equal 'true', <<~'RUBY', frozen_string_literal: false
|
|
port = Ractor::Port.new
|
|
r = Ractor.new(port) do |port|
|
|
mod = Module.new
|
|
mod.set_temporary_name('worker_module')
|
|
mod.instance_variable_set(:@iv, 'str')
|
|
def mod.to_s = raise('to_s must not be called')
|
|
port << mod
|
|
Ractor.receive
|
|
end
|
|
mod = port.receive
|
|
|
|
begin
|
|
messages = [[:instance_variable_set, :@iv, 'other'], [:remove_instance_variable, :@iv]].map do |args|
|
|
begin
|
|
mod.public_send(*args)
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
end
|
|
messages == ["can not set instance variables of classes/modules created by another Ractor (@iv from worker_module)"] * 2
|
|
ensure
|
|
r << nil
|
|
r.join
|
|
end
|
|
RUBY
|
|
|
|
# normal and JIT-compiled ivar reads report the same names without calling to_s
|
|
assert_equal 'true', <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
@iv = 'str'
|
|
def self.read = @iv
|
|
def self.to_s = raise('to_s must not be called')
|
|
end
|
|
|
|
Ractor.new do
|
|
messages = []
|
|
begin
|
|
C.instance_variable_get(:@iv)
|
|
rescue Ractor::IsolationError => e
|
|
messages << e.message
|
|
end
|
|
40.times do
|
|
begin
|
|
C.read
|
|
rescue Ractor::IsolationError => e
|
|
messages << e.message
|
|
end
|
|
end
|
|
messages == ["can not get unshareable values from instance variables of classes/modules created by another Ractor (@iv from C)"] * 41
|
|
end.value
|
|
RUBY
|
|
|
|
# setting an ivar on a shareable but unfrozen object is not allowed, by instance_variable_set
|
|
assert_equal "can't modify instance variables of a shareable Ractor", %q{
|
|
shared = Ractor.new{}
|
|
|
|
begin
|
|
shared.instance_variable_set(:@iv, 'str')
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# setting an ivar on a shareable but unfrozen object is not allowed, by @iv = ...
|
|
assert_equal "can't modify instance variables of a shareable Ractor", %q{
|
|
class Ractor
|
|
def setup
|
|
@foo = ''
|
|
end
|
|
end
|
|
|
|
shared = Ractor.new{}
|
|
|
|
begin
|
|
shared.setup
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# ivars of a shareable object are frozen, so they can be read from a non-main Ractor
|
|
assert_equal 'nil', %q{
|
|
shared = Ractor.new{}
|
|
|
|
r = Ractor.new shared do |shared|
|
|
shared.instance_variable_get(:@iv)
|
|
end
|
|
|
|
r.value.inspect
|
|
}
|
|
|
|
# setting an ivar on a shareable object is not allowed from a non-main Ractor either
|
|
assert_equal "can't modify instance variables of a shareable Ractor", %q{
|
|
class Ractor
|
|
def setup
|
|
@foo = ''
|
|
end
|
|
end
|
|
|
|
shared = Ractor.new{}
|
|
|
|
r = Ractor.new shared do |shared|
|
|
shared.setup
|
|
end
|
|
|
|
begin
|
|
r.value
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# a Proc which already has ivars is fine: the ractor gets a copy without them
|
|
assert_equal 'ok', %q{
|
|
pr = Proc.new{ :ok }
|
|
pr.instance_variable_set(:@iv, Object.new)
|
|
|
|
Ractor.new(&pr).value.to_s
|
|
}
|
|
|
|
# ... also when it is frozen
|
|
assert_equal 'ok', %q{
|
|
pr = Proc.new{ :ok }
|
|
pr.instance_variable_set(:@iv, Object.new)
|
|
pr.freeze
|
|
|
|
Ractor.new(&pr).value.to_s
|
|
}
|
|
|
|
# an ivar set by Proc#refined is internal, so it does not block isolation
|
|
assert_equal 'r', %q{
|
|
module M
|
|
refine String do
|
|
def foo; 'r'; end
|
|
end
|
|
end
|
|
|
|
rp = Proc.new{ ''.foo }.refined(M)
|
|
Ractor.new(&rp).value
|
|
}
|
|
|
|
# a Ractor's &:sym under refinements stays alive across its own GCs: it must not be
|
|
# cached in the main Ractor's refinements table, which that GC does not see
|
|
assert_equal 'ok', %q{
|
|
module M
|
|
refine(Object) { def zzz = 1 }
|
|
end
|
|
using M
|
|
|
|
def f = [[1], []].reject(&:empty?)
|
|
|
|
Ractor.new do
|
|
100_000.times { f; Object.new; "x" * 100 }
|
|
:ok
|
|
end.value.to_s
|
|
}
|
|
|
|
# Ractor.new does not make the given Proc shareable, so it can still be given ivars
|
|
assert_equal "false", %q{
|
|
HAX = -> { }
|
|
Ractor.new(&HAX).join
|
|
|
|
HAX.instance_variable_set(:@foo, Object.new)
|
|
Ractor.shareable?(HAX).to_s
|
|
}
|
|
|
|
# freezing a shareable object from another Ractor can not expose an ivar, since
|
|
# none could be set after it became shareable
|
|
assert_equal 'nil', %q{
|
|
HAX = Ractor.shareable_lambda { }
|
|
|
|
r = Ractor.new do
|
|
HAX.freeze
|
|
HAX.instance_variable_get(:@foo)
|
|
end
|
|
|
|
r.value.inspect
|
|
}
|
|
|
|
# But a shareable object is frozen, it is allowed to access ivars from non-main Ractor
|
|
assert_equal '11', %q{
|
|
[Object.new, [], ].map{|obj|
|
|
obj.instance_variable_set('@a', 1)
|
|
Ractor.make_shareable obj = obj.freeze
|
|
|
|
Ractor.new obj do |obj|
|
|
obj.instance_variable_get('@a')
|
|
end.value.to_s
|
|
}.join
|
|
}
|
|
|
|
# and instance variables of classes/modules are accessible if they refer shareable objects
|
|
assert_equal '333', %q{
|
|
class C
|
|
@int = 1
|
|
@str = '-1000'.dup
|
|
@fstr = '100'.freeze
|
|
|
|
def self.int = @int
|
|
def self.str = @str
|
|
def self.fstr = @fstr
|
|
end
|
|
|
|
module M
|
|
@int = 2
|
|
@str = '-2000'.dup
|
|
@fstr = '200'.freeze
|
|
|
|
def self.int = @int
|
|
def self.str = @str
|
|
def self.fstr = @fstr
|
|
end
|
|
|
|
a = Ractor.new{ C.int }.value
|
|
b = Ractor.new do
|
|
C.str.to_i
|
|
rescue Ractor::IsolationError
|
|
10
|
|
end.value
|
|
c = Ractor.new do
|
|
C.fstr.to_i
|
|
end.value
|
|
|
|
d = Ractor.new{ M.int }.value
|
|
e = Ractor.new do
|
|
M.str.to_i
|
|
rescue Ractor::IsolationError
|
|
20
|
|
end.value
|
|
f = Ractor.new do
|
|
M.fstr.to_i
|
|
end.value
|
|
|
|
|
|
# 1 + 10 + 100 + 2 + 20 + 200
|
|
a + b + c + d + e + f
|
|
}
|
|
|
|
assert_equal '["instance-variable", "instance-variable", nil]', %q{
|
|
class C
|
|
@iv1 = ""
|
|
@iv2 = 42
|
|
def self.iv1 = defined?(@iv1) # "instance-variable"
|
|
def self.iv2 = defined?(@iv2) # "instance-variable"
|
|
def self.iv3 = defined?(@iv3) # nil
|
|
end
|
|
|
|
Ractor.new{
|
|
[C.iv1, C.iv2, C.iv3]
|
|
}.value
|
|
}
|
|
|
|
# moved objects have their shape properly set to original object's shape
|
|
assert_equal '1234', %q{
|
|
class Obj
|
|
attr_accessor :a, :b, :c, :d
|
|
def initialize
|
|
@a = 1
|
|
@b = 2
|
|
@c = 3
|
|
end
|
|
end
|
|
r = Ractor.new do
|
|
obj = receive
|
|
obj.d = 4
|
|
[obj.a, obj.b, obj.c, obj.d]
|
|
end
|
|
obj = Obj.new
|
|
r.send(obj, move: true)
|
|
values = r.value
|
|
values.join
|
|
}
|
|
|
|
# Reading non-shareable cvar of a class created by another Ractor is not allowed
|
|
assert_equal 'can not read non-shareable class variable @@cv of C, which was created by another Ractor', %q{
|
|
class C
|
|
@@cv = 'str'
|
|
end
|
|
|
|
r = Ractor.new do
|
|
class C
|
|
p @@cv
|
|
end
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# also cached non-shareable cvar read of a foreign class is not allowed
|
|
assert_equal 'can not read non-shareable class variable @@cv of C, which was created by another Ractor', %q{
|
|
class C
|
|
@@cv = 'str'
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
C.cv # cache
|
|
|
|
r = Ractor.new do
|
|
C.cv
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Reading shareable cvar from non-main Ractor is allowed
|
|
assert_equal 'shareable', %q{
|
|
class C
|
|
@@cv = 'shareable'.freeze
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
Ractor.new { C.cv }.value
|
|
}
|
|
|
|
# Reading shareable cvar (integer) from non-main Ractor is allowed
|
|
assert_equal '42', %q{
|
|
class C
|
|
@@cv = 42
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
Ractor.new { C.cv }.value.to_s
|
|
}
|
|
|
|
# Reading shareable cvar via module include from non-main Ractor is allowed
|
|
assert_equal 'hello', %q{
|
|
module M
|
|
@@cv = 'hello'.freeze
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
class C
|
|
include M
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
Ractor.new { C.cv }.value
|
|
}
|
|
|
|
# Writing a cvar of a class created by another Ractor is not allowed
|
|
assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{
|
|
class C
|
|
@@cv = 'str'
|
|
def self.cv=(v)
|
|
@@cv = v
|
|
end
|
|
end
|
|
|
|
r = Ractor.new do
|
|
C.cv = 'new'
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Reading cvar that was made shareable after initial assignment
|
|
assert_equal 'made shareable', %q{
|
|
class C
|
|
@@cv = +'made shareable'
|
|
Ractor.make_shareable(@@cv)
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
Ractor.new { C.cv }.value
|
|
}
|
|
|
|
# cvar_defined? works from non-main Ractor
|
|
assert_equal 'true', %q{
|
|
class C
|
|
@@cv = 42
|
|
def self.cv?
|
|
defined?(@@cv)
|
|
end
|
|
end
|
|
|
|
r = Ractor.new { C.cv? ? 'true' : 'false' }
|
|
r.value
|
|
}
|
|
|
|
# A shareable object belongs to no single Ractor, so its singleton class is the
|
|
# main Ractor's rather than the one which materialized it
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
begin
|
|
Ractor.main.define_singleton_method(:zzz) { :sub }
|
|
:not_raised
|
|
rescue Ractor::IsolationError
|
|
:raised
|
|
end
|
|
end
|
|
raised = r.value
|
|
Ractor.main.define_singleton_method(:zzz) { :main }
|
|
(raised == :raised && Ractor.main.zzz == :main).to_s
|
|
}
|
|
|
|
# The constant inline cache is keyed on the Ractor which filled it, so a non-owner
|
|
# can not be handed an unshareable constant through a cache the owner primed
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
port = Ractor::Port.new
|
|
r = Ractor.new(port) do |port|
|
|
k = Class.new
|
|
k.const_set(:X, [1, 2, 3])
|
|
m = Module.new
|
|
m.const_set(:K, k)
|
|
m.module_eval("def self.rd = K::X")
|
|
m.rd # the owner fills the cache
|
|
port << m
|
|
Ractor.receive
|
|
end
|
|
m = port.receive
|
|
res = begin
|
|
m.rd
|
|
'no error'
|
|
rescue Ractor::IsolationError
|
|
'Ractor::IsolationError'
|
|
end
|
|
r << nil
|
|
r.join
|
|
res
|
|
}
|
|
|
|
# Module#refine writes its refinement tables into the receiver, so the receiver
|
|
# must be owned too
|
|
assert_equal 'can not modify M because it is created by another Ractor', %q{
|
|
module M; end
|
|
|
|
r = Ractor.new do
|
|
M.send(:refine, Class.new) { def z = 1 }
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# ... and so does Module#ruby2_keywords, which rewrites a method definition
|
|
assert_equal 'can not modify M because it is created by another Ractor', %q{
|
|
module M
|
|
def m(*a) = a
|
|
end
|
|
|
|
r = Ractor.new do
|
|
M.send(:ruby2_keywords, :m)
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Changing method visibility on a class created by another Ractor is not allowed
|
|
assert_equal 'can not modify C because it is created by another Ractor', %q{
|
|
class C
|
|
def m; end
|
|
end
|
|
|
|
r = Ractor.new do
|
|
C.send(:private, :m)
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# ... and neither is module_function with a method name
|
|
assert_equal 'can not modify M because it is created by another Ractor', %q{
|
|
module M
|
|
def m; end
|
|
end
|
|
|
|
r = Ractor.new do
|
|
M.send(:module_function, :m)
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Visibility changes on a class the Ractor created itself are allowed
|
|
assert_equal 'true', %q{
|
|
Ractor.new do
|
|
k = Class.new { def m; end }
|
|
k.send(:private, :m)
|
|
k.private_method_defined?(:m).to_s
|
|
end.value
|
|
}
|
|
|
|
# A moved object takes its singleton class's ownership with it, so the receiver
|
|
# can keep defining singleton methods on it
|
|
assert_equal '[:from_main, :from_ractor]', %q{
|
|
o = Object.new
|
|
def o.foo = :from_main # materializes the singleton class here, in the main Ractor
|
|
|
|
r = Ractor.new do
|
|
x = Ractor.receive
|
|
def x.bar = :from_ractor
|
|
[x.foo, x.bar]
|
|
end
|
|
r.send(o, move: true)
|
|
r.value.inspect
|
|
}
|
|
|
|
# ... the whole eigenclass chain goes with it, so `class << obj.singleton_class`
|
|
# keeps working for the receiver
|
|
assert_equal 'true', %q{
|
|
o = Object.new
|
|
def o.foo = :main
|
|
o.singleton_class.singleton_class # materialize it here, in the main Ractor
|
|
|
|
port = Ractor::Port.new
|
|
r = Ractor.new(port) do |port|
|
|
x = Ractor.receive
|
|
x.singleton_class.singleton_class.define_method(:mm) { :sub }
|
|
port << x.singleton_class.mm
|
|
end
|
|
r.send(o, move: true)
|
|
res = port.receive
|
|
r.join
|
|
(res == :sub).to_s
|
|
}
|
|
|
|
# ... but the move is refused when that singleton class holds unshareable values,
|
|
# which the sender would keep while the receiver became their owner
|
|
assert_equal 'can not move an object whose singleton class has variable @iv referring to an unshareable object', <<~'RUBY', frozen_string_literal: false
|
|
o = Object.new
|
|
o.singleton_class.instance_variable_set(:@iv, 'sender')
|
|
|
|
r = Ractor.new { Ractor.receive }
|
|
msg = begin
|
|
r.send(o, move: true)
|
|
'no error'
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
r.send(1)
|
|
r.join
|
|
msg
|
|
RUBY
|
|
|
|
# ... and it comes back when the object is moved back
|
|
assert_equal '[:from_main, :from_ractor, :from_main_again]', %q{
|
|
o = Object.new
|
|
def o.foo = :from_main
|
|
|
|
port = Ractor::Port.new
|
|
r = Ractor.new(port) do |port|
|
|
x = Ractor.receive
|
|
def x.bar = :from_ractor
|
|
port.send(x, move: true)
|
|
end
|
|
r.send(o, move: true)
|
|
|
|
back = port.receive
|
|
def back.baz = :from_main_again
|
|
[back.foo, back.bar, back.baz].inspect
|
|
}
|
|
|
|
# A Ractor has full use of the cvars of a class it created, unshareable values included
|
|
assert_equal 'not shareable', %q{
|
|
Ractor.new do
|
|
k = Class.new
|
|
k.class_variable_set(:@@cv, +'not shareable')
|
|
k.class_variable_get(:@@cv)
|
|
end.value
|
|
}
|
|
|
|
# The owner is the owner of the class the cvar is stored in, not of the receiver
|
|
assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{
|
|
class C
|
|
@@cv = 1
|
|
end
|
|
|
|
r = Ractor.new do
|
|
# a subclass created by this Ractor, but @@cv lives in C
|
|
Class.new(C).class_variable_set(:@@cv, 2)
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Removing a cvar of a class created by another Ractor is not allowed
|
|
assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{
|
|
class C
|
|
@@cv = 1
|
|
end
|
|
|
|
r = Ractor.new do
|
|
C.send(:remove_class_variable, :@@cv)
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Freezing a class created by another Ractor is not allowed
|
|
assert_equal 'can not modify String because it is created by another Ractor', %q{
|
|
r = Ractor.new do
|
|
String.freeze
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Freezing a class the Ractor created itself is allowed, and freezing an
|
|
# already frozen class stays a no-op for everybody
|
|
assert_equal 'true true', %q{
|
|
FROZEN = Class.new.freeze
|
|
|
|
Ractor.new do
|
|
own = Class.new
|
|
own.freeze
|
|
"#{own.frozen?} #{FROZEN.freeze.frozen?}"
|
|
end.value
|
|
}
|
|
|
|
# set_temporary_name on a class created by another Ractor is not allowed
|
|
assert_equal 'can not modify C because it is created by another Ractor', %q{
|
|
class C; end
|
|
|
|
r = Ractor.new do
|
|
C.set_temporary_name('other')
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# set_temporary_name on a module the Ractor created itself is allowed
|
|
assert_equal 'mine', %q{
|
|
Ractor.new do
|
|
Module.new.set_temporary_name('mine').name
|
|
end.value
|
|
}
|
|
|
|
# Getting non-shareable objects via constants by other Ractors is not allowed
|
|
assert_equal 'can not access non-shareable objects in constant C::CONST of a class/module created by another Ractor.', <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
CONST = 'str'
|
|
end
|
|
r = Ractor.new do
|
|
C::CONST
|
|
end
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# Constant cache should care about non-shareable constants
|
|
assert_equal "can not access non-shareable objects in constant Object::STR of a class/module created by another Ractor.", <<~'RUBY', frozen_string_literal: false
|
|
STR = "hello"
|
|
def str; STR; end
|
|
s = str() # fill const cache
|
|
begin
|
|
Ractor.new{ str() }.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# The correct constant path shall be reported
|
|
assert_equal "can not access non-shareable objects in constant Object::STR of a class/module created by another Ractor.", <<~'RUBY', frozen_string_literal: false
|
|
STR = "hello"
|
|
module M
|
|
def self.str; STR; end
|
|
end
|
|
|
|
begin
|
|
Ractor.new{ M.str }.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# Copying a class/module created by another Ractor is not allowed: the copy would
|
|
# carry the owner's field values
|
|
assert_equal 'can not copy a class/module created by another Ractor', %q{
|
|
class C
|
|
CONST = 1
|
|
end
|
|
|
|
r = Ractor.new { C.dup }
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
module M; end
|
|
Ractor.new { M.clone rescue $!.class.to_s }.value
|
|
}
|
|
|
|
# Setting constants of classes created by other Ractors is not allowed
|
|
assert_equal 'can not set constants of classes/modules created by another Ractor', <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
end
|
|
r = Ractor.new do
|
|
C::CONST = 'str'
|
|
end
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
RUBY
|
|
|
|
# define_method is not allowed
|
|
assert_equal "defined with an un-shareable Proc in a different Ractor", %q{
|
|
str = "foo"
|
|
define_method(:buggy){|i| str << "#{i}"}
|
|
begin
|
|
Ractor.new{buggy(10)}.join
|
|
rescue => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Immutable Array and Hash are shareable, so it can be shared with constants
|
|
assert_equal '[1000, 3]', %q{
|
|
A = Array.new(1000).freeze # [nil, ...]
|
|
H = {a: 1, b: 2, c: 3}.freeze
|
|
|
|
Ractor.new{ [A.size, H.size] }.value
|
|
}
|
|
|
|
# Ractor.count
|
|
assert_equal '[1, 4, 3, 2, 1]', %q{
|
|
counts = []
|
|
counts << Ractor.count
|
|
ractors = (1..3).map { Ractor.new { Ractor.receive } }
|
|
counts << Ractor.count
|
|
|
|
ractors[0].send('End 0').join
|
|
sleep 0.1 until ractors[0].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
ractors[1].send('End 1').join
|
|
sleep 0.1 until ractors[1].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
ractors[2].send('End 2').join
|
|
sleep 0.1 until ractors[2].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
counts.inspect
|
|
}
|
|
|
|
# ObjectSpace.each_object enumerates the calling Ractor's own objects (unshareable ones
|
|
# included) and other Ractors' shareable objects, but never their unshareable ones.
|
|
assert_equal 'true', %q{
|
|
Ractor.new{
|
|
own = Object.new
|
|
seen = false
|
|
ObjectSpace.each_object{|o| seen = true if o.equal?(own)}
|
|
seen
|
|
}.value
|
|
}
|
|
|
|
# Ractor.make_shareable(obj)
|
|
assert_equal 'true', <<~'RUBY', frozen_string_literal: false
|
|
class C
|
|
def initialize
|
|
@a = 'foo'
|
|
@b = 'bar'
|
|
end
|
|
|
|
def freeze
|
|
@c = [:freeze_called]
|
|
super
|
|
end
|
|
|
|
attr_reader :a, :b, :c
|
|
end
|
|
S = Struct.new(:s1, :s2)
|
|
str = "hello"
|
|
str.instance_variable_set("@iv", "hello")
|
|
/a/ =~ 'a'
|
|
m = $~
|
|
class N < Numeric
|
|
def /(other)
|
|
1
|
|
end
|
|
end
|
|
ary = []; ary << ary
|
|
|
|
a = [[1, ['2', '3']],
|
|
{Object.new => "hello"},
|
|
C.new,
|
|
S.new("x", "y"),
|
|
("a".."b"),
|
|
str,
|
|
ary, # cycle
|
|
/regexp/,
|
|
/#{'r'.upcase}/,
|
|
m,
|
|
Complex(N.new,0),
|
|
Rational(N.new,0),
|
|
true,
|
|
false,
|
|
nil,
|
|
1, 1.2, 1+3r, 1+4i, # Numeric
|
|
]
|
|
Ractor.make_shareable(a)
|
|
|
|
# check all frozen
|
|
a.each{|o|
|
|
raise o.inspect unless o.frozen?
|
|
|
|
case o
|
|
when C
|
|
raise o.a.inspect unless o.a.frozen?
|
|
raise o.b.inspect unless o.b.frozen?
|
|
raise o.c.inspect unless o.c.frozen? && o.c == [:freeze_called]
|
|
when Rational
|
|
raise o.numerator.inspect unless o.numerator.frozen?
|
|
when Complex
|
|
raise o.real.inspect unless o.real.frozen?
|
|
when Array
|
|
if o[0] == 1
|
|
raise o[1][1].inspect unless o[1][1].frozen?
|
|
end
|
|
when Hash
|
|
o.each{|k, v|
|
|
raise k.inspect unless k.frozen?
|
|
raise v.inspect unless v.frozen?
|
|
}
|
|
end
|
|
}
|
|
|
|
Ractor.shareable?(a)
|
|
RUBY
|
|
|
|
# Ractor.make_shareable(obj) doesn't freeze shareable objects
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new{}
|
|
Ractor.make_shareable(a = [r])
|
|
[a.frozen?, a[0].frozen?] == [true, false]
|
|
}
|
|
|
|
# Ractor.make_shareable(a_proc) requires a shareable receiver
|
|
assert_equal '[:ok, "Proc\'s self is not shareable:"]', %q{
|
|
pr1 = nil.instance_exec { Proc.new{} }
|
|
pr2 = Proc.new{}
|
|
|
|
[pr1, pr2].map do |pr|
|
|
begin
|
|
Ractor.make_shareable(pr)
|
|
rescue Ractor::Error => e
|
|
e.message[/^.+?:/]
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
}
|
|
|
|
# Ractor.make_shareable(Method/UnboundMethod)
|
|
assert_equal 'true', %q{
|
|
# raise because receiver is unshareable
|
|
begin
|
|
_m0 = Ractor.make_shareable(self.method(:__id__))
|
|
rescue => e
|
|
raise e unless e.message =~ /can not make shareable object/
|
|
else
|
|
raise "no error"
|
|
end
|
|
|
|
# Method with shareable receiver
|
|
M1 = Ractor.make_shareable(Object.method(:__id__))
|
|
|
|
# UnboundMethod
|
|
M2 = Ractor.make_shareable(Object.instance_method(:__id__))
|
|
|
|
Ractor.new do
|
|
Object.__id__ == M1.call && M1.call == M2.bind_call(Object)
|
|
end.value
|
|
}
|
|
|
|
# Ractor.shareable?(recursive_objects)
|
|
assert_equal '[false, false]', %q{
|
|
y = []
|
|
x = [y, {}].freeze
|
|
y << x
|
|
y.freeze
|
|
[Ractor.shareable?(x), Ractor.shareable?(y)]
|
|
}
|
|
|
|
# Ractor.make_shareable(recursive_objects)
|
|
assert_equal '[:ok, false, false]', %q{
|
|
o = Object.new
|
|
def o.freeze; raise; end
|
|
y = []
|
|
x = [y, o].freeze
|
|
y << x
|
|
y.freeze
|
|
[(Ractor.make_shareable(x) rescue :ok), Ractor.shareable?(x), Ractor.shareable?(y)]
|
|
}
|
|
|
|
# Ractor.make_shareable with Class/Module
|
|
assert_equal '[C, M]', %q{
|
|
class C; end
|
|
module M; end
|
|
|
|
Ractor.make_shareable(ary = [C, M])
|
|
}
|
|
|
|
# define_method() can invoke different Ractor's proc if the proc is shareable.
|
|
assert_equal '1', %q{
|
|
class C
|
|
a = 1
|
|
define_method "foo", Ractor.shareable_proc{ a }
|
|
end
|
|
|
|
Ractor.new{ C.new.foo }.value
|
|
}
|
|
|
|
# Ractor.make_shareable(obj, copy: true) makes copied shareable object.
|
|
assert_equal '[false, false, true, true]', %q{
|
|
r = []
|
|
o1 = [1, 2, ["3"]]
|
|
|
|
o2 = Ractor.make_shareable(o1, copy: true)
|
|
r << Ractor.shareable?(o1) # false
|
|
r << (o1.object_id == o2.object_id) # false
|
|
|
|
o3 = Ractor.make_shareable(o1)
|
|
r << Ractor.shareable?(o1) # true
|
|
r << (o1.object_id == o3.object_id) # false
|
|
r
|
|
}
|
|
|
|
# TracePoint with normal Proc should be Ractor local
|
|
assert_equal '[6, 10]', %q{
|
|
rs = []
|
|
TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do
|
|
Ractor.new{ # line 5
|
|
a = 1
|
|
b = 2
|
|
}.value
|
|
c = 3 # line 9
|
|
end
|
|
rs
|
|
}
|
|
|
|
# Ractor deep copies frozen objects (ary)
|
|
assert_equal '[true, false]', %q{
|
|
Ractor.new([[]].freeze) { |ary|
|
|
[ary.frozen?, ary.first.frozen? ]
|
|
}.value
|
|
}
|
|
|
|
# Ractor deep copies frozen objects (str)
|
|
assert_equal '[true, false]', %q{
|
|
s = String.new.instance_eval { @x = []; freeze}
|
|
Ractor.new(s) { |s|
|
|
[s.frozen?, s.instance_variable_get(:@x).frozen?]
|
|
}.value
|
|
}
|
|
|
|
# Can not trap with a Proc referring to unshareable objects on non-main ractor
|
|
assert_equal '[:ok, :ok, :ok]', %q{
|
|
a = []
|
|
Ractor.new{
|
|
trap(:INT){p :ok}
|
|
}.join
|
|
a << :ok
|
|
|
|
begin
|
|
Ractor.new{
|
|
s = 'str'
|
|
trap(:INT){p s}
|
|
}.join
|
|
rescue Ractor::RemoteError
|
|
a << :ok
|
|
end
|
|
|
|
# a read-only shareable outer variable is fine
|
|
msg = 'shareable'.freeze
|
|
Ractor.new{
|
|
trap(:INT){p msg}
|
|
}.join
|
|
a << :ok
|
|
}
|
|
|
|
# Ractor.select is interruptible
|
|
assert_normal_exit %q{
|
|
trap(:INT) do
|
|
exit
|
|
end
|
|
|
|
r = Ractor.new do
|
|
loop do
|
|
sleep 1
|
|
end
|
|
end
|
|
|
|
Thread.new do
|
|
sleep 0.5
|
|
Process.kill(:INT, Process.pid)
|
|
end
|
|
Ractor.select(r)
|
|
}
|
|
|
|
# Ractor-local storage
|
|
assert_equal '[nil, "b", "a"]', %q{
|
|
ans = []
|
|
Ractor.current[:key] = 'a'
|
|
r = Ractor.new{
|
|
Ractor.main << self[:key]
|
|
self[:key] = 'b'
|
|
self[:key]
|
|
}
|
|
ans << Ractor.receive
|
|
ans << r.value
|
|
ans << Ractor.current[:key]
|
|
}
|
|
|
|
assert_equal '1', %q{
|
|
N = 100
|
|
Ractor.new{
|
|
a = []
|
|
100.times.map{|i|
|
|
Thread.new(i){|i|
|
|
Thread.pass if i < N
|
|
a << Ractor.store_if_absent(:i){ i }
|
|
a << Ractor.current[:i]
|
|
}
|
|
}.each(&:join)
|
|
a.uniq.size
|
|
}.value
|
|
}
|
|
|
|
# Ractor-local storage
|
|
assert_equal '2', %q{
|
|
Ractor.new {
|
|
fails = 0
|
|
begin
|
|
Ractor.main[:key] # cannot get ractor local storage from non-main ractor
|
|
rescue => e
|
|
fails += 1 if e.message =~ /Cannot get ractor local/
|
|
end
|
|
begin
|
|
Ractor.main[:key] = 'val'
|
|
rescue => e
|
|
fails += 1 if e.message =~ /Cannot set ractor local/
|
|
end
|
|
fails
|
|
}.value
|
|
}
|
|
|
|
###
|
|
### Synchronization tests
|
|
###
|
|
|
|
N = 100_000
|
|
|
|
# fstring pool
|
|
assert_equal "#{N}#{N}", %Q{
|
|
N = #{N}
|
|
2.times.map{
|
|
Ractor.new{
|
|
N.times{|i| -(i.to_s)}
|
|
}
|
|
}.map{|r| r.value}.join
|
|
}
|
|
|
|
assert_equal "ok", %Q{
|
|
N = #{N}
|
|
a, b = 2.times.map{
|
|
Ractor.new{
|
|
N.times.map{|i| -(i.to_s)}
|
|
}
|
|
}.map{|r| r.value}
|
|
N.times do |i|
|
|
unless a[i].equal?(b[i])
|
|
raise [a[i], b[i]].inspect
|
|
end
|
|
unless a[i] == i.to_s
|
|
raise [i, a[i], b[i]].inspect
|
|
end
|
|
end
|
|
:ok
|
|
}
|
|
|
|
# Generic fields_tbl
|
|
n = N/2
|
|
assert_equal "#{n}#{n}", %Q{
|
|
2.times.map{
|
|
Ractor.new do
|
|
#{n}.times do
|
|
obj = +''
|
|
obj.instance_variable_set("@a", 1)
|
|
obj.instance_variable_set("@b", 1)
|
|
obj.instance_variable_set("@c", 1)
|
|
obj.instance_variable_defined?("@a")
|
|
end
|
|
end
|
|
}.map{|r| r.value}.join
|
|
}
|
|
|
|
# Now NoMethodError is copyable
|
|
assert_equal "NoMethodError", %q{
|
|
obj = "".freeze # NameError refers the receiver indirectly
|
|
begin
|
|
obj.bar
|
|
rescue => err
|
|
end
|
|
|
|
r = Ractor.new{ Ractor.receive }
|
|
r << err
|
|
r.value.class
|
|
}
|
|
|
|
assert_equal "ok", %q{
|
|
GC.disable
|
|
Ractor.new {}
|
|
raise "not ok" unless GC.disable
|
|
|
|
foo = []
|
|
10.times { foo << 1 }
|
|
|
|
GC.start
|
|
|
|
'ok'
|
|
}
|
|
|
|
# Can yield back values while GC is sweeping [Bug #18117]
|
|
assert_equal "ok", %q{
|
|
port = Ractor::Port.new
|
|
workers = (0...8).map do
|
|
Ractor.new port do |port|
|
|
loop do
|
|
10_000.times.map { Object.new }
|
|
port << Time.now
|
|
Ractor.receive
|
|
end
|
|
end
|
|
end
|
|
|
|
100.times {
|
|
workers.each do
|
|
port.receive
|
|
end
|
|
workers.each do |w|
|
|
w.send(nil)
|
|
end
|
|
}
|
|
"ok"
|
|
} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky
|
|
|
|
# check method cache invalidation
|
|
assert_equal "ok", %q{
|
|
module M
|
|
def foo
|
|
@foo
|
|
end
|
|
end
|
|
|
|
class A
|
|
include M
|
|
|
|
def initialize
|
|
100.times { |i| instance_variable_set(:"@var_#{i}", "bad: #{i}") }
|
|
@foo = 2
|
|
end
|
|
end
|
|
|
|
class B
|
|
include M
|
|
|
|
def initialize
|
|
@foo = 1
|
|
end
|
|
end
|
|
|
|
Ractor.new do
|
|
b = B.new
|
|
100_000.times do
|
|
raise unless b.foo == 1
|
|
end
|
|
end
|
|
|
|
a = A.new
|
|
100_000.times do
|
|
raise unless a.foo == 2
|
|
end
|
|
|
|
"ok"
|
|
}
|
|
|
|
# check method cache invalidation
|
|
assert_equal 'true', %q{
|
|
class C1; def self.foo = 1; end
|
|
class C2; def self.foo = 2; end
|
|
class C3; def self.foo = 3; end
|
|
class C4; def self.foo = 5; end
|
|
class C5; def self.foo = 7; end
|
|
class C6; def self.foo = 11; end
|
|
class C7; def self.foo = 13; end
|
|
class C8; def self.foo = 17; end
|
|
|
|
LN = 10_000
|
|
RN = 10
|
|
CS = [C1, C2, C3, C4, C5, C6, C7, C8]
|
|
rs = RN.times.map{|i|
|
|
Ractor.new(CS.shuffle){|cs|
|
|
LN.times.sum{
|
|
cs.inject(1){|r, c| r * c.foo} # c.foo invalidates method cache entry
|
|
}
|
|
}
|
|
}
|
|
|
|
n = CS.inject(1){|r, c| r * c.foo} * LN
|
|
rs.map{|r| r.value} == Array.new(RN){n}
|
|
}
|
|
|
|
# check method cache invalidation
|
|
# (the owner Ractor redefines methods while another Ractor calls them)
|
|
assert_equal 'true', %q{
|
|
class Foo
|
|
def hello = nil
|
|
end
|
|
|
|
r2 = Ractor.new do
|
|
1000.times do
|
|
o = Foo.new
|
|
o.hello
|
|
end
|
|
end
|
|
|
|
1000.times do
|
|
class Foo
|
|
def hello = nil
|
|
end
|
|
end
|
|
|
|
r2.value
|
|
|
|
true
|
|
}
|
|
|
|
# check experimental warning
|
|
assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor API is experimental/, %q{
|
|
Warning[:experimental] = $VERBOSE = true
|
|
STDERR.reopen(STDOUT)
|
|
eval("Ractor.new{}.value", nil, "test_ractor.rb", 1)
|
|
}, frozen_string_literal: false
|
|
|
|
# check moved object
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
Ractor.receive
|
|
GC.start
|
|
:ok
|
|
end
|
|
|
|
obj = begin
|
|
raise
|
|
rescue => e
|
|
e = Marshal.load(Marshal.dump(e))
|
|
end
|
|
|
|
r.send obj, move: true
|
|
r.value
|
|
}
|
|
|
|
## Ractor::Selector
|
|
|
|
# Selector#empty? returns true
|
|
assert_equal 'true', %q{
|
|
skip true unless defined? Ractor::Selector
|
|
|
|
s = Ractor::Selector.new
|
|
s.empty?
|
|
}
|
|
|
|
# Selector#empty? returns false if there is target ractors
|
|
assert_equal 'false', %q{
|
|
skip false unless defined? Ractor::Selector
|
|
|
|
s = Ractor::Selector.new
|
|
s.add Ractor.new{}
|
|
s.empty?
|
|
}
|
|
|
|
# Selector#clear removes all ractors from the waiting list
|
|
assert_equal 'true', %q{
|
|
skip true unless defined? Ractor::Selector
|
|
|
|
s = Ractor::Selector.new
|
|
s.add Ractor.new{10}
|
|
s.add Ractor.new{20}
|
|
s.clear
|
|
s.empty?
|
|
}
|
|
|
|
# Selector#wait can wait multiple ractors
|
|
assert_equal '[10, 20, true]', %q{
|
|
skip [10, 20, true] unless defined? Ractor::Selector
|
|
|
|
s = Ractor::Selector.new
|
|
s.add Ractor.new{10}
|
|
s.add Ractor.new{20}
|
|
r, v = s.wait
|
|
vs = []
|
|
vs << v
|
|
r, v = s.wait
|
|
vs << v
|
|
[*vs.sort, s.empty?]
|
|
} if defined? Ractor::Selector
|
|
|
|
# Selector#wait can wait multiple ractors with receiving.
|
|
assert_equal '30', %q{
|
|
skip 30 unless defined? Ractor::Selector
|
|
|
|
RN = 30
|
|
rs = RN.times.map{
|
|
Ractor.new{ :v }
|
|
}
|
|
s = Ractor::Selector.new(*rs)
|
|
|
|
results = []
|
|
until s.empty?
|
|
results << s.wait
|
|
|
|
# Note that s.wait can raise an exception because other Ractors/Threads
|
|
# can take from the same ractors in the waiting set.
|
|
# In this case there is no other takers so `s.wait` doesn't raise an error.
|
|
end
|
|
|
|
results.size
|
|
} if defined? Ractor::Selector
|
|
|
|
# Selector#wait can support dynamic addition
|
|
assert_equal '600', %q{
|
|
skip 600 unless defined? Ractor::Selector
|
|
|
|
RN = 100
|
|
s = Ractor::Selector.new
|
|
port = Ractor::Port.new
|
|
rs = RN.times.map{
|
|
Ractor.new{
|
|
Ractor.main << Ractor.new(port){|port| port << :v3; :v4 }
|
|
Ractor.main << Ractor.new(port){|port| port << :v5; :v6 }
|
|
Ractor.yield :v1
|
|
:v2
|
|
}
|
|
}
|
|
|
|
rs.each{|r| s.add(r)}
|
|
h = {v1: 0, v2: 0, v3: 0, v4: 0, v5: 0, v6: 0}
|
|
|
|
loop do
|
|
case s.wait receive: true
|
|
in :receive, r
|
|
s.add r
|
|
in r, v
|
|
h[v] += 1
|
|
break if h.all?{|k, v| v == RN}
|
|
end
|
|
end
|
|
|
|
h.sum{|k, v| v}
|
|
} unless yjit_enabled? # http://ci.rvm.jp/results/trunk-yjit@ruby-sp2-docker/4466770
|
|
|
|
# Selector should be GCed (free'ed) without trouble
|
|
assert_equal 'ok', %q{
|
|
skip :ok unless defined? Ractor::Selector
|
|
|
|
RN = 30
|
|
rs = RN.times.map{
|
|
Ractor.new{ :v }
|
|
}
|
|
s = Ractor::Selector.new(*rs)
|
|
:ok
|
|
}
|
|
|
|
end # if !ENV['GITHUB_WORKFLOW']
|
|
|
|
# Chilled strings are not shareable
|
|
assert_equal 'false', %q{
|
|
Ractor.shareable?("chilled")
|
|
}
|
|
|
|
# Chilled strings can be made shareable
|
|
assert_equal 'true', %q{
|
|
shareable = Ractor.make_shareable("chilled")
|
|
shareable == "chilled" && Ractor.shareable?(shareable)
|
|
}
|
|
|
|
# require in Ractor
|
|
assert_equal 'true', %q{
|
|
Module.new do
|
|
def require feature
|
|
return Ractor._require(feature) unless Ractor.main?
|
|
super
|
|
end
|
|
Object.prepend self
|
|
set_temporary_name 'Ractor#require'
|
|
end
|
|
|
|
Ractor.new{
|
|
begin
|
|
require 'tempfile'
|
|
Tempfile.new
|
|
rescue SystemStackError
|
|
# prism parser with -O0 build consumes a lot of machine stack
|
|
Data.define(:fileno).new(1)
|
|
end
|
|
}.value.fileno > 0
|
|
}
|
|
|
|
# require_relative in Ractor
|
|
assert_equal 'true', %q{
|
|
dummyfile = File.join(__dir__, "dummy#{rand}.rb")
|
|
return true if File.exist?(dummyfile)
|
|
|
|
begin
|
|
File.write dummyfile, ''
|
|
rescue Exception
|
|
# skip on any errors
|
|
return true
|
|
end
|
|
|
|
begin
|
|
Ractor.new dummyfile do |f|
|
|
require_relative File.basename(f)
|
|
end.value
|
|
ensure
|
|
File.unlink dummyfile
|
|
end
|
|
}
|
|
|
|
# require_relative in Ractor
|
|
assert_equal 'LoadError', %q{
|
|
dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
|
|
return true if File.exist?(dummyfile)
|
|
|
|
Ractor.new dummyfile do |f|
|
|
begin
|
|
require_relative File.basename(f)
|
|
rescue LoadError => e
|
|
e.class
|
|
end
|
|
end.value
|
|
}
|
|
|
|
# autolaod in Ractor
|
|
assert_equal 'true', %q{
|
|
autoload :Tempfile, 'tempfile'
|
|
|
|
r = Ractor.new do
|
|
begin
|
|
Tempfile.new
|
|
rescue SystemStackError
|
|
# prism parser with -O0 build consumes a lot of machine stack
|
|
Data.define(:fileno).new(1)
|
|
end
|
|
end
|
|
r.value.fileno > 0
|
|
}
|
|
|
|
# failed in autolaod in Ractor
|
|
assert_equal 'LoadError', %q{
|
|
dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
|
|
autoload :Tempfile, dummyfile
|
|
|
|
r = Ractor.new do
|
|
begin
|
|
Tempfile.new
|
|
rescue LoadError => e
|
|
e.class
|
|
end
|
|
end
|
|
r.value
|
|
}
|
|
|
|
# bind_call in Ractor [Bug #20934]
|
|
assert_equal 'ok', %q{
|
|
2.times.map do
|
|
Ractor.new do
|
|
1000.times do
|
|
Object.instance_method(:itself).bind_call(self)
|
|
end
|
|
end
|
|
end.each(&:join)
|
|
GC.start
|
|
:ok.itself
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (String)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = "foobarbazfoobarbazfoobarbazfoobarbaz"
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (Array)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (Hash)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = { foo: 1, bar: 2 }
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (MatchData)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = "foo".match(/o/)
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (Struct)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Struct.new(:a, :b, :c, :d, :e, :f).new(1, 2, 3, 4, 5, 6)
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved objects being corrupted if embeded (Object)
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
class SomeObject
|
|
attr_reader :a, :b, :c, :d, :e, :f
|
|
def initialize
|
|
@a = @b = @c = @d = @e = @f = 1
|
|
end
|
|
|
|
def ==(o)
|
|
@a == o.a &&
|
|
@b == o.b &&
|
|
@c == o.c &&
|
|
@d == o.d &&
|
|
@e == o.e &&
|
|
@f == o.f
|
|
end
|
|
end
|
|
|
|
SomeObject.new # initial non-embeded
|
|
|
|
obj = SomeObject.new
|
|
ractor.send(obj.dup, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj == obj ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved arrays can't be used
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = [1]
|
|
ractor.send(obj, move: true)
|
|
begin
|
|
[].concat(obj)
|
|
rescue TypeError
|
|
:ok
|
|
else
|
|
:fail
|
|
end
|
|
}
|
|
|
|
# moved strings can't be used
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = "hello"
|
|
ractor.send(obj, move: true)
|
|
begin
|
|
"".replace(obj)
|
|
rescue TypeError
|
|
:ok
|
|
else
|
|
:fail
|
|
end
|
|
}
|
|
|
|
# moved hashes can't be used
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = { a: 1 }
|
|
ractor.send(obj, move: true)
|
|
begin
|
|
{}.merge(obj)
|
|
rescue TypeError
|
|
:ok
|
|
else
|
|
:fail
|
|
end
|
|
}
|
|
|
|
# move objects inside frozen containers
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
original = obj.dup
|
|
ractor.send([obj].freeze, move: true)
|
|
roundtripped_obj = ractor.value[0]
|
|
roundtripped_obj == original ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# move object with generic ivar
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
obj.instance_variable_set(:@array, [1])
|
|
|
|
ractor.send(obj, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj.instance_variable_get(:@array) == [1] ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# move object with many generic ivars
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
0.upto(300) do |i|
|
|
obj.instance_variable_set(:"@array#{i}", [i])
|
|
end
|
|
|
|
ractor.send(obj, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# move object with complex generic ivars
|
|
assert_equal 'ok', %q{
|
|
# Make Array complex
|
|
30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) }
|
|
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
obj.instance_variable_set(:@array1, [1])
|
|
|
|
ractor.send(obj, move: true)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# copy object with complex generic ivars
|
|
assert_equal 'ok', %q{
|
|
# Make Array complex
|
|
30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) }
|
|
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
obj.instance_variable_set(:@array1, [1])
|
|
|
|
ractor.send(obj)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# copy object with many generic ivars
|
|
assert_equal 'ok', %q{
|
|
ractor = Ractor.new { Ractor.receive }
|
|
obj = Array.new(10, 42)
|
|
0.upto(300) do |i|
|
|
obj.instance_variable_set(:"@array#{i}", [i])
|
|
end
|
|
|
|
ractor.send(obj)
|
|
roundtripped_obj = ractor.value
|
|
roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
|
|
}
|
|
|
|
# moved composite types move their non-shareable parts properly
|
|
assert_equal 'ok', %q{
|
|
k, v = String.new("key"), String.new("value")
|
|
h = { k => v }
|
|
h.instance_variable_set("@b", String.new("b"))
|
|
a = [k,v]
|
|
o_singleton = Object.new
|
|
def o_singleton.a
|
|
@a
|
|
end
|
|
o_singleton.instance_variable_set("@a", String.new("a"))
|
|
class MyObject
|
|
attr_reader :a
|
|
def initialize(a)
|
|
@a = a
|
|
end
|
|
end
|
|
struct_class = Struct.new(:a)
|
|
struct = struct_class.new(String.new('a'))
|
|
o = MyObject.new(String.new('a'))
|
|
port = Ractor::Port.new
|
|
|
|
r = Ractor.new port do |port|
|
|
loop do
|
|
obj = Ractor.receive
|
|
val = case obj
|
|
when Hash
|
|
obj['key'] == 'value' && obj.instance_variable_get("@b") == 'b'
|
|
when Array
|
|
obj[0] == 'key'
|
|
when Struct
|
|
obj.a == 'a'
|
|
when Object
|
|
obj.a == 'a'
|
|
end
|
|
port << val
|
|
end
|
|
end
|
|
|
|
objs = [h, a, o_singleton, o, struct]
|
|
objs.each_with_index do |obj, i|
|
|
klass = obj.class
|
|
parts_moved = {}
|
|
case obj
|
|
when Hash
|
|
parts_moved[klass] = [obj['key'], obj.instance_variable_get("@b")]
|
|
when Array
|
|
parts_moved[klass] = obj.dup # the contents
|
|
when Struct, Object
|
|
parts_moved[klass] = [obj.a]
|
|
end
|
|
r.send(obj, move: true)
|
|
val = port.receive
|
|
if val != true
|
|
raise "bad val in ractor for obj at i:#{i}"
|
|
end
|
|
begin
|
|
p obj
|
|
rescue
|
|
else
|
|
raise "should be moved"
|
|
end
|
|
parts_moved.each do |klass, parts|
|
|
parts.each_with_index do |part, j|
|
|
case part
|
|
when Ractor::MovedObject
|
|
else
|
|
raise "part for class #{klass} at i:#{j} should be moved"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
'ok'
|
|
}
|
|
|
|
# fork after creating Ractor
|
|
assert_equal 'ok', %q{
|
|
begin
|
|
Ractor.new { Ractor.receive }
|
|
_, status = Process.waitpid2 fork { }
|
|
status.success? ? "ok" : status
|
|
rescue NotImplementedError
|
|
:ok
|
|
end
|
|
}
|
|
|
|
# Ractors should be terminated after fork
|
|
assert_equal 'ok', %q{
|
|
begin
|
|
r = Ractor.new { Ractor.receive }
|
|
_, status = Process.waitpid2 fork {
|
|
begin
|
|
raise if r.value != nil
|
|
end
|
|
}
|
|
r.send(123)
|
|
raise unless r.value == 123
|
|
status.success? ? "ok" : status
|
|
rescue NotImplementedError
|
|
:ok
|
|
end
|
|
}
|
|
|
|
# Ractors should be terminated after fork
|
|
assert_equal 'ok', %q{
|
|
begin
|
|
r = Ractor.new { Ractor.receive }
|
|
_, status = Process.waitpid2 fork {
|
|
begin
|
|
r.send(123)
|
|
rescue Ractor::ClosedError
|
|
end
|
|
}
|
|
r.send(123)
|
|
raise unless r.value == 123
|
|
status.success? ? "ok" : status
|
|
rescue NotImplementedError
|
|
:ok
|
|
end
|
|
}
|
|
|
|
# Creating classes inside of Ractors
|
|
# [Bug #18119]
|
|
assert_equal 'ok', %q{
|
|
port = Ractor::Port.new
|
|
workers = (0...8).map do
|
|
Ractor.new port do |port|
|
|
loop do
|
|
100.times.map { Class.new }
|
|
port << nil
|
|
end
|
|
end
|
|
end
|
|
|
|
100.times { port.receive }
|
|
|
|
'ok'
|
|
}
|
|
|
|
# Using Symbol#to_proc inside ractors
|
|
# [Bug #21354]
|
|
assert_equal 'ok', %q{
|
|
:inspect.to_proc
|
|
Ractor.new do
|
|
# It should not use this cached proc, it should create a new one. If it used
|
|
# the cached proc, we would get a ractor_confirm_belonging error here.
|
|
:inspect.to_proc
|
|
end.join
|
|
'ok'
|
|
}
|
|
|
|
# take vm lock when deleting generic ivars from the global table
|
|
assert_equal 'ok', %q{
|
|
Ractor.new do
|
|
a = [1, 2, 3]
|
|
a.object_id
|
|
a.dup # this deletes generic ivar on dupped object
|
|
'ok'
|
|
end.value
|
|
}
|
|
|
|
## Ractor#monitor
|
|
|
|
# monitor port returns [ractor, :exited] when the monitering Ractor terminated.
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
Ractor.main << :ok1
|
|
:ok2
|
|
end
|
|
|
|
r.monitor port = Ractor::Port.new
|
|
Ractor.receive # :ok1
|
|
port.receive == [r, :exited]
|
|
}
|
|
|
|
# monitor port returns [ractor, :exited] even if the monitoring Ractor was terminated.
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
:ok
|
|
end
|
|
|
|
r.join # wait for r's terminateion
|
|
|
|
r.monitor port = Ractor::Port.new
|
|
port.receive == [r, :exited]
|
|
}
|
|
|
|
# monitor returns false if the monitoring Ractor was terminated.
|
|
assert_equal 'false', %q{
|
|
r = Ractor.new do
|
|
:ok
|
|
end
|
|
|
|
r.join # wait for r's terminateion
|
|
|
|
r.monitor Ractor::Port.new
|
|
}
|
|
|
|
# monitor port returns [ractor, :aborted] when the monitering Ractor is aborted.
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
Ractor.main << :ok1
|
|
raise 'ok'
|
|
end
|
|
|
|
r.monitor port = Ractor::Port.new
|
|
Ractor.receive # :ok1
|
|
port.receive == [r, :aborted]
|
|
}
|
|
|
|
# monitor port returns [ractor, :aborted] even if the monitoring Ractor was aborted.
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
raise 'ok'
|
|
end
|
|
|
|
begin
|
|
r.join # wait for r's terminateion
|
|
rescue Ractor::RemoteError
|
|
# ignore
|
|
end
|
|
|
|
r.monitor port = Ractor::Port.new
|
|
port.receive == [r, :aborted]
|
|
}
|
|
|
|
assert_equal 'ok', %q{
|
|
long = Ractor.new { Ractor.receive }
|
|
Ractor.new(long) { |t| t.monitor(Ractor::Port.new) }.value
|
|
GC.start
|
|
'ok'
|
|
}
|
|
|
|
assert_equal 'ok', %q{
|
|
long = Ractor.new { Ractor.receive }
|
|
20.times do
|
|
Ractor.new(long) { |t| t.monitor(Ractor::Port.new) }.value
|
|
GC.start
|
|
end
|
|
long.send(:bye)
|
|
long.join
|
|
'ok'
|
|
}
|
|
|
|
## Ractor#join
|
|
|
|
# Ractor#join returns self when the Ractor is terminated.
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
Ractor.receive
|
|
end
|
|
|
|
r << :ok
|
|
r.join
|
|
r.inspect in /terminated/
|
|
} if false # TODO
|
|
|
|
# Ractor#join raises RemoteError when the remote Ractor aborted with an exception
|
|
assert_equal 'err', %q{
|
|
r = Ractor.new do
|
|
raise 'err'
|
|
end
|
|
|
|
begin
|
|
r.join
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
## Ractor#value
|
|
|
|
# Ractor#value returns the last expression even if it is unshareable
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
obj = [1, 2]
|
|
obj << obj.object_id
|
|
end
|
|
|
|
ret = r.value
|
|
ret == [1, 2, ret.object_id]
|
|
}
|
|
|
|
# Only one Ractor can call Ractor#value, and only once
|
|
assert_equal '[["Only the successor ractor can take a value", 9], ["The value was already taken", 1], ["ok", 1]]', %q{
|
|
r = Ractor.new do
|
|
'ok'
|
|
end
|
|
|
|
RN = 10
|
|
|
|
rs = RN.times.map do
|
|
Ractor.new r do |r|
|
|
begin
|
|
Ractor.main << r.value
|
|
Ractor.main << r.value # the value is taken only once
|
|
rescue Ractor::Error => e
|
|
Ractor.main << e.message
|
|
end
|
|
end
|
|
end
|
|
|
|
(RN+1).times.map{
|
|
Ractor.receive
|
|
}.tally.sort
|
|
}
|
|
|
|
# Cause lots of inline CC misses.
|
|
assert_equal 'ok', <<~'RUBY'
|
|
class A; def test; 1 + 1; end; end
|
|
class B; def test; 1 + 1; end; end
|
|
class C; def test; 1 + 1; end; end
|
|
class D; def test; 1 + 1; end; end
|
|
class E; def test; 1 + 1; end; end
|
|
class F; def test; 1 + 1; end; end
|
|
class G; def test; 1 + 1; end; end
|
|
|
|
objs = [A.new, B.new, C.new, D.new, E.new, F.new, G.new].freeze
|
|
|
|
def call_test(obj)
|
|
obj.test
|
|
end
|
|
|
|
ractors = 7.times.map do
|
|
Ractor.new(objs) do |objs|
|
|
objs = objs.shuffle
|
|
100_000.times do
|
|
objs.each do |o|
|
|
call_test(o)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
ractors.each(&:join)
|
|
:ok
|
|
RUBY
|
|
|
|
# This test checks that we do not trigger a GC when we have malloc with Ractor
|
|
# locks. We cannot trigger a GC with Ractor locks because GC requires VM lock
|
|
# and Ractor barrier. If another Ractor is waiting on this Ractor lock, then it
|
|
# will deadlock because the other Ractor will never join the barrier.
|
|
#
|
|
# Creating Ractor::Port requires locking the Ractor and inserting into an
|
|
# st_table, which can call malloc.
|
|
assert_equal 'ok', <<~'RUBY'
|
|
r = Ractor.new do
|
|
loop do
|
|
Ractor::Port.new
|
|
end
|
|
end
|
|
|
|
10.times do
|
|
10_000.times do
|
|
r.send(nil)
|
|
end
|
|
sleep(0.01)
|
|
end
|
|
:ok
|
|
RUBY
|
|
|
|
assert_equal 'ok', <<~'RUBY'
|
|
begin
|
|
100.times do |i|
|
|
Ractor.new(i) do |j|
|
|
1000.times do |i|
|
|
"#{j}-#{i}"
|
|
end
|
|
Ractor.receive
|
|
end
|
|
pid = fork { }
|
|
_, status = Process.waitpid2 pid
|
|
raise unless status.success?
|
|
end
|
|
|
|
:ok
|
|
rescue NotImplementedError
|
|
:ok
|
|
end
|
|
RUBY
|
|
|
|
assert_equal 'ok', <<~'RUBY'
|
|
begin
|
|
100.times do |i|
|
|
Ractor.new(i) do |j|
|
|
1000.times do |i|
|
|
"#{j}-#{i}"
|
|
end
|
|
end
|
|
pid = fork do
|
|
GC.verify_internal_consistency
|
|
end
|
|
_, status = Process.waitpid2 pid
|
|
raise unless status.success?
|
|
end
|
|
|
|
:ok
|
|
rescue NotImplementedError
|
|
:ok
|
|
end
|
|
RUBY
|
|
|
|
# When creating bmethods in Ractors, they should only be usable from their
|
|
# defining ractor, even if it is GC'd
|
|
assert_equal 'ok', <<~'RUBY'
|
|
|
|
begin
|
|
# Each Ractor creates its own class (it can only define bmethods on classes
|
|
# it owns) and returns it after defining the bmethod.
|
|
# This would be better to run in parallel, but there's a bug with lambda
|
|
# creation and YJIT causing crashes in dev mode
|
|
ractors = 1000.times.map do
|
|
Ractor.new do
|
|
klass = Class.new
|
|
Ractor.receive
|
|
klass.define_method(:foo) {}
|
|
klass
|
|
end
|
|
end
|
|
|
|
CLASSES = ractors.map do |ractor|
|
|
ractor << nil
|
|
ractor.value
|
|
end.freeze
|
|
|
|
ractors.clear
|
|
GC.start
|
|
|
|
any = 1000.times.map do
|
|
Ractor.new do
|
|
CLASSES.any? do |klass|
|
|
begin
|
|
klass.new.foo
|
|
true
|
|
rescue RuntimeError
|
|
false
|
|
end
|
|
end
|
|
end
|
|
end.map(&:value).none? && :ok
|
|
rescue ThreadError => e
|
|
# ignore limited memory machine
|
|
if /can\'t create Thread/ =~ e.message
|
|
:ok
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
RUBY
|
|
|
|
# Concurrent super calls with keyword arguments must not race on the
|
|
# callinfo kwarg reference count. [Bug #22075]
|
|
assert_equal 'ok', %q{
|
|
class Base
|
|
def foo(a:, b:, c:) = a
|
|
end
|
|
|
|
class Sub < Base
|
|
def foo(a:, b:, c:) = super(a: a, b: b, c: c)
|
|
end
|
|
|
|
4.times.map do
|
|
Ractor.new do
|
|
obj = Sub.new
|
|
100_000.times { obj.foo(a: 1, b: 2, c: 3) }
|
|
end
|
|
end.each(&:join)
|
|
|
|
:ok
|
|
}
|
|
|
|
# A thread terminating (here: the pipe writer) while another Ractor runs a
|
|
# compaction barrier must not corrupt the machine context of a thread blocked
|
|
# in IO, whose locked read buffer lives only on that machine stack.
|
|
assert_equal 'ok', %q{
|
|
Warning[:experimental] = false
|
|
can_compact = begin
|
|
GC.compact
|
|
true
|
|
rescue NotImplementedError
|
|
false
|
|
end
|
|
if can_compact
|
|
b = Ractor.new do
|
|
10.times do
|
|
r, w = IO.pipe
|
|
t = Thread.new { w.write("x" * 40); w.close }
|
|
r.read
|
|
r.close
|
|
t.join
|
|
end
|
|
:ok
|
|
end
|
|
a = Ractor.new { 20.times { GC.compact }; :ok }
|
|
[a, b].each(&:value)
|
|
end
|
|
:ok
|
|
}
|
|
|
|
# Forking while other Ractors are alive takes a VM barrier in the parent; the
|
|
# child inherits that scheduler/barrier state and must reset it. Exercises the
|
|
# parent-side fork barrier and the child teardown, then checks the surviving
|
|
# Ractors still work.
|
|
assert_equal 'ok', %q{
|
|
begin
|
|
rs = 5.times.map { Ractor.new { Ractor.receive } }
|
|
10.times do
|
|
pid = fork { GC.start }
|
|
_, status = Process.waitpid2(pid)
|
|
raise "child failed" unless status.success?
|
|
end
|
|
rs.each { |r| r.send(nil) }
|
|
rs.each(&:value)
|
|
:ok
|
|
rescue NotImplementedError
|
|
:ok # platform without fork
|
|
end
|
|
}
|
|
|
|
# A moved object's source is neutralized into a RactorMovedObject husk that
|
|
# stays in its original (possibly larger) slot. It must be given a shape whose
|
|
# slot size matches that slot, or a later compaction in the receiver trips the
|
|
# slot_size == shape_slot_size invariant (RGENGC_CHECK_MODE) / corrupts the slot.
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
while (o = Ractor.receive)
|
|
begin
|
|
GC.compact
|
|
rescue NotImplementedError
|
|
# no-op on platforms without GC.compact (e.g. MMTk)
|
|
end
|
|
end
|
|
:ok
|
|
end
|
|
500.times do
|
|
o = Object.new
|
|
12.times { |i| o.instance_variable_set("@i#{i}", i) } # overflow to a larger slot
|
|
r.send(o, move: true)
|
|
end
|
|
r.send(nil)
|
|
r.value
|
|
}
|
|
|
|
# Moving a CoW shared-root string (a frozen root with an unshareable ivar) must not steal the
|
|
# root's buffer; that would leave the remaining sharers reading freed memory.
|
|
assert_equal 'ok', %q{
|
|
30.times do
|
|
r = Ractor.new do
|
|
v = Ractor.receive
|
|
v.bytesize
|
|
:done
|
|
end
|
|
f = "x" * 4096
|
|
f.instance_variable_set(:@x, []) # unshareable ivar: moved rather than passed through
|
|
f.freeze
|
|
g = f.dup # shares f's buffer, making f a shared root
|
|
h = f[10, 3000] # a long substring shares the buffer too
|
|
r.send(f, move: true)
|
|
r.value
|
|
GC.start
|
|
10.times { "z" * 4096 }
|
|
raise "sharer corrupted" unless g == "x" * 4096 && h == "x" * 3000
|
|
end
|
|
:ok
|
|
}
|
|
|
|
# Same for an array: a frozen array is a shared root without carrying the shared root flag,
|
|
# so its buffer belongs to the sharers and the move must not free it.
|
|
assert_equal 'ok', %q{
|
|
30.times do
|
|
r = Ractor.new do
|
|
v = Ractor.receive
|
|
v.size
|
|
:done
|
|
end
|
|
a = (1..100).to_a
|
|
a.instance_variable_set(:@x, []) # unshareable ivar: moved rather than passed through
|
|
a.freeze
|
|
b = a.dup # shares a's buffer, making a a shared root
|
|
c = a[10, 80] # a subseq shares the buffer too
|
|
r.send(a, move: true)
|
|
r.value
|
|
GC.start
|
|
10.times { (1..100).to_a }
|
|
raise "sharer corrupted" unless b == (1..100).to_a && c == (11..90).to_a
|
|
end
|
|
:ok
|
|
}
|
|
|
|
# Moving a String/Array/Hash subclass (an unshareable ivar sends it down the move path) must
|
|
# preserve the class rather than degrading it to the base class.
|
|
assert_equal '["MyStr", "MyAry", "MyHash"]', %q{
|
|
class MyStr < String; end
|
|
class MyAry < Array; end
|
|
class MyHash < Hash; end
|
|
r = Ractor.new do
|
|
3.times.map { Ractor.receive.class.name }
|
|
end
|
|
[MyStr.new("x"), (MyAry.new << 1), (h=MyHash.new; h[:a]=1; h)].each do |o|
|
|
o.instance_variable_set(:@x, []) # unshareable ivar sends it down the move path
|
|
r.send(o, move: true)
|
|
end
|
|
r.value.inspect
|
|
}
|
|
|
|
# Moving an object with a singleton class must keep its singleton methods and reattach the
|
|
# rebuilt singleton class to the new object; otherwise it keeps pointing at the original the
|
|
# sender's attach invalidated. Covers T_OBJECT, String and Struct.
|
|
assert_equal '[[:obj, true], [:str, true], [:strct, true]]', %q{
|
|
o = Object.new
|
|
def o.m; :obj end
|
|
s = +"str"
|
|
def s.m; :str end
|
|
st = Struct.new(:a).new(1)
|
|
def st.m; :strct end
|
|
r = Ractor.new do
|
|
3.times.map do
|
|
v = Ractor.receive
|
|
GC.start
|
|
[v.m, v.method(:m).owner.attached_object.equal?(v)]
|
|
end
|
|
end
|
|
[o, s, st].each { |x| r.send(x, move: true) }
|
|
r.value.inspect
|
|
}
|
|
|
|
# A port that is still reachable keeps its queue, whether or not it was closed. (Dropping
|
|
# the queue of a port that is gone is not fixed for mmtk, so that case is a test-all one
|
|
# that omits itself: see test_port_queue_dropped_when_port_unreachable.)
|
|
assert_equal '[[0, 1, 2], [:a, :b]]', %q{
|
|
taken = 3.times.map do |i|
|
|
port = Ractor::Port.new
|
|
Ractor.new(port, i) { |p, n| p << n; nil }.join
|
|
port
|
|
end
|
|
closed = Ractor::Port.new
|
|
closed.send(:a); closed.send(:b); closed.close
|
|
6.times { GC.start }
|
|
[taken.map(&:receive), [closed.receive, closed.receive]]
|
|
}
|
|
|
|
# Closing a port wakes a receiver waiting on it, with or without a timeout.
|
|
assert_equal '[:closed, :closed]', %q{
|
|
untimed = Ractor::Port.new
|
|
th = Thread.new do
|
|
begin
|
|
untimed.receive
|
|
rescue Ractor::ClosedError
|
|
:closed
|
|
end
|
|
end
|
|
Thread.pass until th.status == 'sleep'
|
|
untimed.close
|
|
untimed_result = th.value # before the next close, which would wake it too
|
|
|
|
timed = Ractor::Port.new
|
|
th2 = Thread.new do
|
|
begin
|
|
timed.receive(timeout: 10)
|
|
rescue Ractor::ClosedError
|
|
:closed
|
|
end
|
|
end
|
|
Thread.pass until th2.status == 'sleep'
|
|
timed.close
|
|
|
|
[untimed_result, th2.value]
|
|
}
|
|
|
|
# last_cwd is set on init in the main Ractor, but Dir.pwd attempts to store into
|
|
# it. When this is done on a child Ractor we violate the invariant that the
|
|
# owning and registering Ractor must be the same. On a normal build the
|
|
# use-after-free is invisible, because we only zero out the flags, so the cache
|
|
# check compares the slot bytes as usual and just thinks it's a cache miss.
|
|
#
|
|
# This test exists because it will hit a use-after-poison on ASAN builds
|
|
assert_equal 'ok', %q{
|
|
Dir.chdir("..")
|
|
Ractor.new { Dir.pwd; 500_000.times { "y" * 300 } }.join
|
|
Dir.pwd
|
|
'ok'
|
|
}
|
|
|
|
# A stale errno left on another native thread must not raise IO::TimeoutError
|
|
assert_equal 'ok', %q{
|
|
8.times.map do
|
|
Ractor.new do
|
|
t = Time.now
|
|
Thread.new do
|
|
r, = IO.pipe
|
|
r.timeout = 0.0001
|
|
(r.read(1) rescue nil) while Time.now - t < 0.1
|
|
end
|
|
r, w = IO.pipe
|
|
Thread.new do
|
|
(w << "x"; sleep 0.0001) while Time.now - t < 0.1
|
|
w.close
|
|
end
|
|
nil while r.read(1)
|
|
end
|
|
end.each(&:join)
|
|
:ok
|
|
}
|
|
|
|
# A Ractor's main thread must not share its ec serial with its threads
|
|
assert_equal 'false', %q{
|
|
rs = 4.times.map {
|
|
Ractor.new {
|
|
m = Thread::Mutex.new
|
|
m.lock
|
|
ok = 8.times.map { Thread.new { m.owned? } }.map(&:value)
|
|
begin
|
|
8.times.map { Thread.new { m.unlock rescue nil } }.each(&:join)
|
|
ensure
|
|
m.unlock
|
|
end
|
|
ok.any?
|
|
}
|
|
}
|
|
rs.map(&:value).any?
|
|
}
|