mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
1621 lines
30 KiB
Ruby
1621 lines
30 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.take
|
|
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.take
|
|
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.take
|
|
}
|
|
|
|
# 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.take
|
|
}
|
|
|
|
# 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.take == ['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.take
|
|
}
|
|
|
|
# Ractor#receive_if can filter the message
|
|
assert_equal '[2, 3, 1]', %q{
|
|
r = Ractor.new Ractor.current do |main|
|
|
main << 1
|
|
main << 2
|
|
main << 3
|
|
end
|
|
a = []
|
|
a << Ractor.receive_if{|msg| msg == 2}
|
|
a << Ractor.receive_if{|msg| msg == 3}
|
|
a << Ractor.receive
|
|
}
|
|
|
|
# Ractor#receive_if with break
|
|
assert_equal '[2, [1, :break], 3]', %q{
|
|
r = Ractor.new Ractor.current do |main|
|
|
main << 1
|
|
main << 2
|
|
main << 3
|
|
end
|
|
|
|
a = []
|
|
a << Ractor.receive_if{|msg| msg == 2}
|
|
a << Ractor.receive_if{|msg| break [msg, :break]}
|
|
a << Ractor.receive
|
|
}
|
|
|
|
# Ractor#receive_if can't be called recursively
|
|
assert_equal '[[:e1, 1], [:e2, 2]]', %q{
|
|
r = Ractor.new Ractor.current do |main|
|
|
main << 1
|
|
main << 2
|
|
main << 3
|
|
end
|
|
|
|
a = []
|
|
|
|
Ractor.receive_if do |msg|
|
|
begin
|
|
Ractor.receive
|
|
rescue Ractor::Error
|
|
a << [:e1, msg]
|
|
end
|
|
true # delete 1 from queue
|
|
end
|
|
|
|
Ractor.receive_if do |msg|
|
|
begin
|
|
Ractor.receive_if{}
|
|
rescue Ractor::Error
|
|
a << [:e2, msg]
|
|
end
|
|
true # delete 2 from queue
|
|
end
|
|
|
|
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(&:take)
|
|
}
|
|
|
|
# Ractor.make_shareable 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 = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } }
|
|
|
|
Ractor.make_shareable(closure).call
|
|
}
|
|
|
|
# Ractor.make_shareable issue for locals in proc [Bug #18023]
|
|
assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{
|
|
a = :a
|
|
closure = Ractor.current.instance_eval do
|
|
-> {
|
|
b, c, d = :b, :c, :d
|
|
-> {
|
|
e, f, g = :e, :f, :g
|
|
-> { [a, b, c, d, e, f, g] }
|
|
}.call
|
|
}.call
|
|
end
|
|
|
|
Ractor.make_shareable(closure).call
|
|
}
|
|
|
|
# Now autoload in non-main Ractor is not supported
|
|
assert_equal 'ok', %q{
|
|
autoload :Foo, 'foo.rb'
|
|
r = Ractor.new do
|
|
p Foo
|
|
rescue Ractor::UnsafeError
|
|
:ok
|
|
end
|
|
r.take
|
|
}
|
|
|
|
###
|
|
###
|
|
# Ractor still has several memory corruption so skip huge number of tests
|
|
if ENV['GITHUB_WORKFLOW'] &&
|
|
ENV['GITHUB_WORKFLOW'] == 'Compilations'
|
|
# ignore the follow
|
|
else
|
|
|
|
# Ractor.select(*ractors) receives a values from a ractors.
|
|
# It is similar to select(2) and Go's select syntax.
|
|
# The return value is [ch, received_value]
|
|
assert_equal 'ok', %q{
|
|
# select 1
|
|
r1 = Ractor.new{'r1'}
|
|
r, obj = Ractor.select(r1)
|
|
'ok' if r == r1 and obj == 'r1'
|
|
}
|
|
|
|
# Ractor.select from two ractors.
|
|
assert_equal '["r1", "r2"]', %q{
|
|
# select 2
|
|
r1 = Ractor.new{'r1'}
|
|
r2 = Ractor.new{'r2'}
|
|
rs = [r1, r2]
|
|
as = []
|
|
r, obj = Ractor.select(*rs)
|
|
rs.delete(r)
|
|
as << obj
|
|
r, obj = Ractor.select(*rs)
|
|
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['RUN_OPTS'] =~ /--jit-min-calls=5/ || # This always fails with --jit-wait --jit-min-calls=5
|
|
(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/, %q{
|
|
begin
|
|
Ractor.select
|
|
rescue ArgumentError => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# Outgoing port of a ractor will be closed when the Ractor is terminated.
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
'finish'
|
|
end
|
|
|
|
r.take
|
|
sleep 0.1 until r.inspect =~ /terminated/
|
|
|
|
begin
|
|
o = r.take
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
"ng: #{o}"
|
|
end
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to send into a terminated ractor
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
end
|
|
|
|
r.take # closed
|
|
sleep 0.1 until r.inspect =~ /terminated/
|
|
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to send into a closed actor
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new { Ractor.receive }
|
|
r.close_incoming
|
|
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to take from closed actor
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
Ractor.yield 1
|
|
Ractor.receive
|
|
end
|
|
|
|
r.close_outgoing
|
|
begin
|
|
r.take
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Can mix with Thread#interrupt and Ractor#take [Bug #17366]
|
|
assert_equal 'err', %q{
|
|
Ractor.new{
|
|
t = Thread.current
|
|
begin
|
|
Thread.new{ t.raise "err" }.join
|
|
rescue => e
|
|
e.message
|
|
end
|
|
}.take
|
|
}
|
|
|
|
# Killed Ractor's thread yields nil
|
|
assert_equal 'nil', %q{
|
|
Ractor.new{
|
|
t = Thread.current
|
|
Thread.new{ t.kill }.join
|
|
}.take.inspect #=> nil
|
|
}
|
|
|
|
# Ractor.yield raises Ractor::ClosedError when outgoing port is closed.
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new Ractor.current do |main|
|
|
Ractor.receive
|
|
main << true
|
|
Ractor.yield 1
|
|
end
|
|
|
|
r.close_outgoing
|
|
r << true
|
|
Ractor.receive
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to send into a ractor with closed incoming port
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new { Ractor.receive }
|
|
r.close_incoming
|
|
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# A ractor with closed incoming port still can send messages out
|
|
assert_equal '[1, 2]', %q{
|
|
r = Ractor.new do
|
|
Ractor.yield 1
|
|
2
|
|
end
|
|
r.close_incoming
|
|
|
|
[r.take, r.take]
|
|
}
|
|
|
|
# Raise Ractor::ClosedError when try to take from a ractor with closed outgoing port
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
Ractor.yield 1
|
|
Ractor.receive
|
|
end
|
|
|
|
sleep 0.01 # wait for Ractor.yield in r
|
|
r.close_outgoing
|
|
begin
|
|
r.take
|
|
rescue Ractor::ClosedError
|
|
'ok'
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# A ractor with closed outgoing port still can receive messages from incoming port
|
|
assert_equal 'ok', %q{
|
|
r = Ractor.new do
|
|
Ractor.receive
|
|
end
|
|
|
|
r.close_outgoing
|
|
begin
|
|
r.send(1)
|
|
rescue Ractor::ClosedError
|
|
'ng'
|
|
else
|
|
'ok'
|
|
end
|
|
}
|
|
|
|
# Ractor.main returns main ractor
|
|
assert_equal 'true', %q{
|
|
Ractor.new{
|
|
Ractor.main
|
|
}.take == Ractor.current
|
|
}
|
|
|
|
# a ractor with closed outgoing port should terminate
|
|
assert_equal 'ok', %q{
|
|
Ractor.new do
|
|
close_outgoing
|
|
end
|
|
|
|
true until Ractor.count == 1
|
|
:ok
|
|
}
|
|
|
|
# multiple Ractors can receive (wait) from one Ractor
|
|
assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{
|
|
pipe = Ractor.new do
|
|
loop do
|
|
Ractor.yield Ractor.receive
|
|
end
|
|
end
|
|
|
|
RN = 10
|
|
rs = RN.times.map{|i|
|
|
Ractor.new pipe, i do |pipe, i|
|
|
msg = pipe.take
|
|
msg # ping-pong
|
|
end
|
|
}
|
|
RN.times{|i|
|
|
pipe << i
|
|
}
|
|
RN.times.map{
|
|
r, n = Ractor.select(*rs)
|
|
rs.delete r
|
|
n
|
|
}.sort
|
|
}
|
|
|
|
# Ractor.select also support multiple take, receive and yield
|
|
assert_equal '[true, true, true]', %q{
|
|
RN = 10
|
|
CR = Ractor.current
|
|
|
|
rs = (1..RN).map{
|
|
Ractor.new do
|
|
CR.send 'send' + CR.take #=> 'sendyield'
|
|
'take'
|
|
end
|
|
}
|
|
received = []
|
|
take = []
|
|
yielded = []
|
|
until rs.empty?
|
|
r, v = Ractor.select(CR, *rs, yield_value: 'yield')
|
|
case r
|
|
when :receive
|
|
received << v
|
|
when :yield
|
|
yielded << v
|
|
else
|
|
take << v
|
|
rs.delete r
|
|
end
|
|
end
|
|
[received.all?('sendyield'), yielded.all?(nil), take.all?('take')]
|
|
}
|
|
|
|
# multiple Ractors can send to one Ractor
|
|
assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{
|
|
pipe = Ractor.new do
|
|
loop do
|
|
Ractor.yield Ractor.receive
|
|
end
|
|
end
|
|
|
|
RN = 10
|
|
RN.times.map{|i|
|
|
Ractor.new pipe, i do |pipe, i|
|
|
pipe << i
|
|
end
|
|
}
|
|
RN.times.map{
|
|
pipe.take
|
|
}.sort
|
|
}
|
|
|
|
# an exception in a Ractor 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.take
|
|
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
|
|
}
|
|
|
|
# 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.take
|
|
}
|
|
|
|
# To copy the object, now Marshal#dump is used
|
|
assert_equal "allocator undefined for Thread", %q{
|
|
obj = Thread.new{}
|
|
begin
|
|
r = Ractor.new obj do |msg|
|
|
msg
|
|
end
|
|
rescue TypeError => e
|
|
e.message #=> no _dump_data is defined for class Thread
|
|
else
|
|
'ng'
|
|
end
|
|
}
|
|
|
|
# send shareable and unshareable objects
|
|
assert_equal "ok", %q{
|
|
echo_ractor = Ractor.new do
|
|
loop do
|
|
v = Ractor.receive
|
|
Ractor.yield 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 = echo_ractor.take
|
|
results << "#{o} is copied" unless o.object_id == o2.object_id
|
|
}
|
|
|
|
unshareable_objects.map{|o|
|
|
echo_ractor << o
|
|
o2 = echo_ractor.take
|
|
results << "#{o.inspect} is not copied" if o.object_id == o2.object_id
|
|
}
|
|
|
|
if results.empty?
|
|
:ok
|
|
else
|
|
results.inspect
|
|
end
|
|
}
|
|
|
|
# frozen Objects are shareable
|
|
assert_equal [false, true, false].inspect, %q{
|
|
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.take
|
|
|
|
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
|
|
}
|
|
|
|
# move example2: String
|
|
# touching moved object causes an error
|
|
assert_equal 'hello world', %q{
|
|
# move
|
|
r = Ractor.new do
|
|
obj = Ractor.receive
|
|
obj << ' world'
|
|
end
|
|
|
|
str = 'hello'
|
|
r.send str, move: true
|
|
modified = r.take
|
|
|
|
begin
|
|
str << ' exception' # raise Ractor::MovedError
|
|
rescue Ractor::MovedError
|
|
modified #=> 'hello world'
|
|
else
|
|
raise 'unreachable'
|
|
end
|
|
}
|
|
|
|
# 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.take
|
|
begin
|
|
a1 << 2 # raise Ractor::MovedError
|
|
rescue Ractor::MovedError
|
|
a2.inspect
|
|
end
|
|
}
|
|
|
|
# move with yield
|
|
assert_equal 'hello', %q{
|
|
r = Ractor.new do
|
|
Thread.current.report_on_exception = false
|
|
obj = 'hello'
|
|
Ractor.yield obj, move: true
|
|
obj << 'world'
|
|
end
|
|
|
|
str = r.take
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError
|
|
str #=> "hello"
|
|
end
|
|
}
|
|
|
|
# yield/move should not make moved object when the yield is not succeeded
|
|
assert_equal '"str"', %q{
|
|
R = Ractor.new{}
|
|
M = Ractor.current
|
|
r = Ractor.new do
|
|
s = 'str'
|
|
selected_r, v = Ractor.select R, yield_value: s, move: true
|
|
raise if selected_r != R # taken from R
|
|
M.send s.inspect # s should not be a moved object
|
|
end
|
|
|
|
Ractor.receive
|
|
}
|
|
|
|
# yield/move can fail
|
|
assert_equal "allocator undefined for Thread", %q{
|
|
r = Ractor.new do
|
|
obj = Thread.new{}
|
|
Ractor.yield obj
|
|
rescue => e
|
|
e.message
|
|
end
|
|
r.take
|
|
}
|
|
|
|
# Access to global-variables are prohibited
|
|
assert_equal 'can not access global variables $gv from non-main Ractors', %q{
|
|
$gv = 1
|
|
r = Ractor.new do
|
|
$gv
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Access to global-variables are prohibited
|
|
assert_equal 'can not access global variables $gv from non-main Ractors', %q{
|
|
r = Ractor.new do
|
|
$gv = 1
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
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.take){|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.take
|
|
}
|
|
|
|
# $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.take
|
|
ractor_local_globals == h #=> true
|
|
}
|
|
|
|
# selfs are different objects
|
|
assert_equal 'false', %q{
|
|
r = Ractor.new do
|
|
self.object_id
|
|
end
|
|
r.take == self.object_id #=> false
|
|
}
|
|
|
|
# self is a Ractor instance
|
|
assert_equal 'true', %q{
|
|
r = Ractor.new do
|
|
self.object_id
|
|
end
|
|
r.object_id == r.take #=> true
|
|
}
|
|
|
|
# given block Proc will be isolated, so can not access outer variables.
|
|
assert_equal 'ArgumentError', %q{
|
|
begin
|
|
a = true
|
|
r = Ractor.new do
|
|
a
|
|
end
|
|
rescue => e
|
|
e.class
|
|
end
|
|
}
|
|
|
|
# ivar in shareable-objects are not allowed to access from non-main Ractor
|
|
assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors", %q{
|
|
class C
|
|
@iv = 'str'
|
|
end
|
|
|
|
r = Ractor.new do
|
|
class C
|
|
p @iv
|
|
end
|
|
end
|
|
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# ivar in shareable-objects are not allowed to access from non-main Ractor
|
|
assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{
|
|
shared = Ractor.new{}
|
|
shared.instance_variable_set(:@iv, 'str')
|
|
|
|
r = Ractor.new shared do |shared|
|
|
p shared.instance_variable_get(:@iv)
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (get)
|
|
assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{
|
|
class Ractor
|
|
def setup
|
|
@foo = ''
|
|
end
|
|
|
|
def foo
|
|
@foo
|
|
end
|
|
end
|
|
|
|
shared = Ractor.new{}
|
|
shared.setup
|
|
|
|
r = Ractor.new shared do |shared|
|
|
p shared.foo
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (set)
|
|
assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{
|
|
class Ractor
|
|
def setup
|
|
@foo = ''
|
|
end
|
|
end
|
|
|
|
shared = Ractor.new{}
|
|
|
|
r = Ractor.new shared do |shared|
|
|
p shared.setup
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# 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.take.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 }.take
|
|
b = Ractor.new do
|
|
C.str.to_i
|
|
rescue Ractor::IsolationError
|
|
10
|
|
end.take
|
|
c = Ractor.new do
|
|
C.fstr.to_i
|
|
end.take
|
|
|
|
d = Ractor.new{ M.int }.take
|
|
e = Ractor.new do
|
|
M.str.to_i
|
|
rescue Ractor::IsolationError
|
|
20
|
|
end.take
|
|
f = Ractor.new do
|
|
M.fstr.to_i
|
|
end.take
|
|
|
|
|
|
# 1 + 10 + 100 + 2 + 20 + 200
|
|
a + b + c + d + e + f
|
|
}
|
|
|
|
# cvar in shareable-objects are not allowed to access from non-main Ractor
|
|
assert_equal 'can not access class variables from non-main Ractors', %q{
|
|
class C
|
|
@@cv = 'str'
|
|
end
|
|
|
|
r = Ractor.new do
|
|
class C
|
|
p @@cv
|
|
end
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# also cached cvar in shareable-objects are not allowed to access from non-main Ractor
|
|
assert_equal 'can not access class variables from non-main Ractors', %q{
|
|
class C
|
|
@@cv = 'str'
|
|
def self.cv
|
|
@@cv
|
|
end
|
|
end
|
|
|
|
C.cv # cache
|
|
|
|
r = Ractor.new do
|
|
C.cv
|
|
end
|
|
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Getting non-shareable objects via constants by other Ractors is not allowed
|
|
assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', %q{
|
|
class C
|
|
CONST = 'str'
|
|
end
|
|
r = Ractor.new do
|
|
C::CONST
|
|
end
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Constant cache should care about non-sharable constants
|
|
assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", %q{
|
|
STR = "hello"
|
|
def str; STR; end
|
|
s = str() # fill const cache
|
|
begin
|
|
Ractor.new{ str() }.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# Setting non-shareable objects into constants by other Ractors is not allowed
|
|
assert_equal 'can not set constants with non-shareable objects by non-main Ractors', %q{
|
|
class C
|
|
end
|
|
r = Ractor.new do
|
|
C::CONST = 'str'
|
|
end
|
|
begin
|
|
r.take
|
|
rescue Ractor::RemoteError => e
|
|
e.cause.message
|
|
end
|
|
}
|
|
|
|
# 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)}.take
|
|
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] }.take
|
|
}
|
|
|
|
# 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').take
|
|
sleep 0.1 until ractors[0].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
ractors[1].send('End 1').take
|
|
sleep 0.1 until ractors[1].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
ractors[2].send('End 2').take
|
|
sleep 0.1 until ractors[2].inspect =~ /terminated/
|
|
counts << Ractor.count
|
|
|
|
counts.inspect
|
|
}
|
|
|
|
# ObjectSpace.each_object can not handle unshareable objects with Ractors
|
|
assert_equal '0', %q{
|
|
Ractor.new{
|
|
n = 0
|
|
ObjectSpace.each_object{|o| n += 1 unless Ractor.shareable?(o)}
|
|
n
|
|
}.take
|
|
}
|
|
|
|
# ObjectSpace._id2ref can not handle unshareable objects with Ractors
|
|
assert_equal 'ok', %q{
|
|
s = 'hello'
|
|
|
|
Ractor.new s.object_id do |id ;s|
|
|
begin
|
|
s = ObjectSpace._id2ref(id)
|
|
rescue => e
|
|
:ok
|
|
end
|
|
end.take
|
|
}
|
|
|
|
# Ractor.make_shareable(obj)
|
|
assert_equal 'true', %q{
|
|
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)
|
|
}
|
|
|
|
# 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) makes a proc shareable.
|
|
assert_equal 'true', %q{
|
|
a = [1, [2, 3], {a: "4"}]
|
|
|
|
pr = Ractor.current.instance_eval do
|
|
Proc.new do
|
|
a
|
|
end
|
|
end
|
|
|
|
Ractor.make_shareable(a) # referred value should be shareable
|
|
Ractor.make_shareable(pr)
|
|
Ractor.shareable?(pr)
|
|
}
|
|
|
|
# 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.make_shareable(Proc.new{ a })
|
|
a = 2
|
|
end
|
|
|
|
Ractor.new{ C.new.foo }.take
|
|
}
|
|
|
|
# Ractor.make_shareable(a_proc) makes a proc shareable.
|
|
assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{
|
|
a = b = nil
|
|
pr = Ractor.current.instance_eval do
|
|
Proc.new do
|
|
c = b # assign to a is okay because c is block local variable
|
|
# reading b is okay
|
|
a = b # assign to a is not allowed #=> Ractor::Error
|
|
end
|
|
end
|
|
|
|
begin
|
|
Ractor.make_shareable(pr)
|
|
rescue => e
|
|
e.message
|
|
end
|
|
}
|
|
|
|
# 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 '[4, 8]', %q{
|
|
rs = []
|
|
TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do
|
|
Ractor.new{ # line 4
|
|
a = 1
|
|
b = 2
|
|
}.take
|
|
c = 3 # line 8
|
|
end
|
|
rs
|
|
}
|
|
|
|
# Ractor deep copies frozen objects (ary)
|
|
assert_equal '[true, false]', %q{
|
|
Ractor.new([[]].freeze) { |ary|
|
|
[ary.frozen?, ary.first.frozen? ]
|
|
}.take
|
|
}
|
|
|
|
# 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?]
|
|
}.take
|
|
}
|
|
|
|
# Can not trap with not isolated Proc on non-main ractor
|
|
assert_equal '[:ok, :ok]', %q{
|
|
a = []
|
|
Ractor.new{
|
|
trap(:INT){p :ok}
|
|
}.take
|
|
a << :ok
|
|
|
|
begin
|
|
Ractor.new{
|
|
s = 'str'
|
|
trap(:INT){p s}
|
|
}.take
|
|
rescue => Ractor::RemoteError
|
|
a << :ok
|
|
end
|
|
}
|
|
|
|
# Ractor-local storage
|
|
assert_equal '[nil, "b", "a"]', %q{
|
|
ans = []
|
|
Ractor.current[:key] = 'a'
|
|
r = Ractor.new{
|
|
Ractor.yield self[:key]
|
|
self[:key] = 'b'
|
|
self[:key]
|
|
}
|
|
ans << r.take
|
|
ans << r.take
|
|
ans << Ractor.current[:key]
|
|
}
|
|
|
|
###
|
|
### 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.take}.join
|
|
}
|
|
|
|
# enc_table
|
|
assert_equal "#{N/10}", %Q{
|
|
Ractor.new do
|
|
loop do
|
|
Encoding.find("test-enc-#{rand(5_000)}").inspect
|
|
rescue ArgumentError => e
|
|
end
|
|
end
|
|
|
|
src = Encoding.find("UTF-8")
|
|
#{N/10}.times{|i|
|
|
src.replicate("test-enc-\#{i}")
|
|
}
|
|
}
|
|
|
|
# Generic ivtbl
|
|
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.take}.join
|
|
}
|
|
|
|
# NameError
|
|
assert_equal "ok", %q{
|
|
begin
|
|
bar
|
|
rescue => err
|
|
end
|
|
begin
|
|
Ractor.new{} << err
|
|
rescue TypeError
|
|
'ok'
|
|
end
|
|
}
|
|
|
|
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{
|
|
workers = (0...8).map do
|
|
Ractor.new do
|
|
loop do
|
|
10_000.times.map { Object.new }
|
|
Ractor.yield Time.now
|
|
end
|
|
end
|
|
end
|
|
|
|
1_000.times { idle_worker, tmp_reporter = Ractor.select(*workers) }
|
|
"ok"
|
|
}
|
|
|
|
assert_equal "ok", %q{
|
|
def foo(*); ->{ super }; end
|
|
begin
|
|
Ractor.make_shareable(foo)
|
|
rescue Ractor::IsolationError
|
|
"ok"
|
|
end
|
|
}
|
|
|
|
assert_equal "ok", %q{
|
|
def foo(**); ->{ super }; end
|
|
begin
|
|
Ractor.make_shareable(foo)
|
|
rescue Ractor::IsolationError
|
|
"ok"
|
|
end
|
|
}
|
|
|
|
assert_equal "ok", %q{
|
|
def foo(...); ->{ super }; end
|
|
begin
|
|
Ractor.make_shareable(foo)
|
|
rescue Ractor::IsolationError
|
|
"ok"
|
|
end
|
|
}
|
|
|
|
assert_equal "ok", %q{
|
|
def foo((x), (y)); ->{ super }; end
|
|
begin
|
|
Ractor.make_shareable(foo([], []))
|
|
rescue Ractor::IsolationError
|
|
"ok"
|
|
end
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
end # if !ENV['GITHUB_WORKFLOW']
|