mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
7622819147
env_copy() uses rb_ary_delete_at() with a loop counting up while iterating through the list of read only locals. rb_ary_delete_at() can shift elements in the array to an index lesser than the loop index, causing locals to be missed and set to Qfalse in the returned environment. Iterate through the locals in reverse instead, this way the shifting never happens for locals that are yet to be visited and we process all the locals in the array. [Bug #18023]
1458 lines
28 KiB
Ruby
1458 lines
28 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 = 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 = -> {
|
|
b, c, d = :b, :c, :d
|
|
-> {
|
|
e, f, g = :e, :f, :g
|
|
-> { [a, b, c, d, e, f, g] }
|
|
}.call
|
|
}.call
|
|
|
|
Ractor.make_shareable(closure).call
|
|
}
|
|
|
|
###
|
|
###
|
|
# 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 access 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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# 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 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 = Proc.new do
|
|
a
|
|
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 = 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
|
|
|
|
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"
|
|
}
|
|
|
|
end # if !ENV['GITHUB_WORKFLOW']
|