Files
Yusuke EndohandClaude Opus 5.5 bde63b65b7 Give a Ractor's main thread an ec serial from the new Ractor
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>
2026-10-25 05:40:21 +09:00

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?
}