# 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 "#", %q{ Ractor.current.inspect } # Return id, loc, and status for no-name ractor assert_match /^#$/, %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 /^#$/, %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.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 } unless /mswin/ =~ RUBY_PLATFORM # randomly hangs on mswin https://github.com/ruby/ruby/actions/runs/3753871445/jobs/6377551069#step:20:131 # 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 = [] taken = [] yielded = [] until received.size == RN && taken.size == RN && yielded.size == RN r, v = Ractor.select(CR, *rs, yield_value: 'yield') case r when :receive received << v when :yield yielded << v else taken << v rs.delete r end end r = [received == ['sendyield'] * RN, yielded == [nil] * RN, taken == ['take'] * RN, ] STDERR.puts [received, yielded, taken].inspect r } # 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 } unless yjit_enabled? # `[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.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", <<~'RUBY', frozen_string_literal: false 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 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.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 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.take 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.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", <<~'RUBY', frozen_string_literal: false 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 RUBY # 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 } # 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.take values.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 } # 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.', <<~'RUBY', frozen_string_literal: false class C CONST = 'str' end r = Ractor.new do C::CONST end begin r.take rescue Ractor::RemoteError => e e.cause.message end RUBY # Constant cache should care about non-sharable constants assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" def str; STR; end s = str() # fill const cache begin Ractor.new{ str() }.take rescue Ractor::RemoteError => e e.cause.message end RUBY # 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', <<~'RUBY', frozen_string_literal: false class C end r = Ractor.new do C::CONST = 'str' end begin r.take 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)}.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', <<~'RUBY', frozen_string_literal: false s = 'hello' Ractor.new s.object_id do |id ;s| begin s = ObjectSpace._id2ref(id) rescue => e :ok end end.take RUBY # 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) 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 '[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 }.take 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? ] }.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 } # 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{ obj = "".freeze # NameError refers the receiver indirectly begin obj.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" } unless yjit_enabled? || rjit_enabled? # flaky 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 } # 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.take} == Array.new(RN){n} } # check experimental warning assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor is experimental/, %q{ Warning[:experimental] = $VERBOSE = true STDERR.reopen(STDOUT) eval("Ractor.new{}.take", 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.take } ## 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 rs = RN.times.map{ Ractor.new{ Ractor.main << Ractor.new{ Ractor.yield :v3; :v4 } Ractor.main << Ractor.new{ Ractor.yield :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) }