mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Since ec serials became Ractor-local (4263f1d718), the root fiber of a
Ractor's main thread gets its serial from the creating Ractor, because it
is allocated there. The serial can then collide with one that the new
Ractor gives to its own threads. Mutex ownership is checked by ec serial,
so another thread could see a Mutex locked by the main thread as its own,
which led to "[BUG] invalid keeping_mutexes".
Ractor.new {
m = Mutex.new
m.lock
Thread.new {}.join
Thread.new { m.unlock }.join # should raise ThreadError
}.join
#=> [BUG] invalid keeping_mutexes: Attempt to unlock a mutex which is not locked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3392 lines
72 KiB
Ruby
3392 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 will be isolated, so can not access outer variables.
|
|
assert_equal 'Ractor::IsolationError', %q{
|
|
begin
|
|
a = true
|
|
r = Ractor.new do
|
|
a
|
|
end
|
|
rescue => e
|
|
e.class
|
|
end
|
|
}
|
|
|
|
# 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 can not be isolated
|
|
assert_equal 'can not isolate a Proc because it has instance variables', %q{
|
|
pr = Proc.new{}
|
|
pr.instance_variable_set(:@iv, Object.new)
|
|
|
|
begin
|
|
Ractor.new(&pr)
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# freezing the Proc first does not skip the check
|
|
assert_equal 'can not isolate a Proc because it has instance variables', %q{
|
|
pr = Proc.new{}
|
|
pr.instance_variable_set(:@iv, Object.new)
|
|
pr.freeze
|
|
|
|
begin
|
|
Ractor.new(&pr)
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# 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 Proc made shareable by Ractor.new can not be given ivars afterwards
|
|
assert_equal "can't modify instance variables of a shareable Proc", %q{
|
|
HAX = -> { }
|
|
Ractor.new(&HAX).join
|
|
|
|
begin
|
|
HAX.instance_variable_set(:@foo, Object.new)
|
|
rescue Ractor::IsolationError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# 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.new(&HAX).join
|
|
|
|
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 not isolated Proc on non-main ractor
|
|
assert_equal '[: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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# A Ractor creation that fails (IsolationError) after the child objspace exists must clean up
|
|
# the creator's cover for it; otherwise a later global GC enumerates the dead child's objspace
|
|
# twice and reads the freed shell.
|
|
assert_equal 'ok', %q{
|
|
x = 42 # capturing an outer local makes Ractor.new raise IsolationError
|
|
worker = Ractor.new { loop { break if Ractor.receive == :quit } }
|
|
begin
|
|
Ractor.new { x }
|
|
raise "isolation error did not fire"
|
|
rescue Ractor::IsolationError
|
|
end
|
|
10.times { GC.start; 500.times { Object.new } }
|
|
worker.send(:quit)
|
|
worker.value
|
|
100.times do |i|
|
|
begin
|
|
Ractor.new { x }
|
|
raise "isolation error did not fire"
|
|
rescue Ractor::IsolationError
|
|
end
|
|
if (i % 20).zero?
|
|
Ractor.new { :ok }.value
|
|
GC.start
|
|
end
|
|
end
|
|
GC.start
|
|
:ok
|
|
}
|
|
|
|
# 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?
|
|
}
|