diff options
Diffstat (limited to 'bootstraptest/test_ractor.rb')
-rw-r--r-- | bootstraptest/test_ractor.rb | 504 |
1 files changed, 451 insertions, 53 deletions
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 6299b50e3b..4868810308 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -187,6 +187,41 @@ assert_equal '[:ok, :ok, :ok]', %q{ }.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 @@ -248,7 +283,7 @@ assert_equal 30.times.map { 'ok' }.to_s, %q{ 30.times.map{|i| test i } -} unless ENV['RUN_OPTS'] =~ /--jit-min-calls=5/ # This always fails with --jit-wait --jit-min-calls=5 +} 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{ @@ -465,7 +500,7 @@ assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ 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{ @@ -479,9 +514,9 @@ assert_equal '[true, true, true]', %q{ end } received = [] - take = [] + taken = [] yielded = [] - until rs.empty? + until received.size == RN && taken.size == RN && yielded.size == RN r, v = Ractor.select(CR, *rs, yield_value: 'yield') case r when :receive @@ -489,11 +524,17 @@ assert_equal '[true, true, true]', %q{ when :yield yielded << v else - take << v + taken << v rs.delete r end end - [received.all?('sendyield'), yielded.all?(nil), take.all?('take')] + r = [received == ['sendyield'] * RN, + yielded == [nil] * RN, + taken == ['take'] * RN, + ] + + STDERR.puts [received, yielded, taken].inspect + r } # multiple Ractors can send to one Ractor @@ -532,7 +573,7 @@ assert_equal '[RuntimeError, "ok", true]', %q{ # threads in a ractor will killed assert_equal '{:ok=>3}', %q{ Ractor.new Ractor.current do |main| - q = Queue.new + q = Thread::Queue.new Thread.new do q << true loop{} @@ -587,7 +628,7 @@ assert_equal "allocator undefined for Thread", %q{ } # send shareable and unshareable objects -assert_equal "ok", %q{ +assert_equal "ok", <<~'RUBY', frozen_string_literal: false echo_ractor = Ractor.new do loop do v = Ractor.receive @@ -654,10 +695,10 @@ assert_equal "ok", %q{ else results.inspect end -} +RUBY # frozen Objects are shareable -assert_equal [false, true, false].inspect, %q{ +assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: false class C def initialize freeze @a = 1 @@ -680,11 +721,11 @@ assert_equal [false, true, false].inspect, %q{ 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', %q{ +assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false # move r = Ractor.new do obj = Ractor.receive @@ -702,7 +743,7 @@ assert_equal 'hello world', %q{ else raise 'unreachable' end -} +RUBY # move example2: Array assert_equal '[0, 1]', %q{ @@ -905,7 +946,7 @@ assert_equal 'ArgumentError', %q{ } # 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{ +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 @@ -916,13 +957,12 @@ assert_equal 'can not access instance variables of classes/modules from non-main 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{ @@ -999,6 +1039,74 @@ assert_equal '11', %q{ }.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 @@ -1018,8 +1126,30 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ 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{ +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 @@ -1031,10 +1161,10 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m 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.", %q{ +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 @@ -1043,10 +1173,10 @@ assert_equal "can not access non-shareable objects in constant Object::STR by no 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', %q{ +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 @@ -1057,10 +1187,10 @@ assert_equal 'can not set constants with non-shareable objects by non-main Racto rescue Ractor::RemoteError => e e.cause.message end -} +RUBY # define_method is not allowed -assert_equal "defined in a different Ractor", %q{ +assert_equal "defined with an un-shareable Proc in a different Ractor", %q{ str = "foo" define_method(:buggy){|i| str << "#{i}"} begin @@ -1110,7 +1240,7 @@ assert_equal '0', %q{ } # ObjectSpace._id2ref can not handle unshareable objects with Ractors -assert_equal 'ok', %q{ +assert_equal 'ok', <<~'RUBY', frozen_string_literal: false s = 'hello' Ractor.new s.object_id do |id ;s| @@ -1120,10 +1250,10 @@ assert_equal 'ok', %q{ :ok end end.take -} +RUBY # Ractor.make_shareable(obj) -assert_equal 'true', %q{ +assert_equal 'true', <<~'RUBY', frozen_string_literal: false class C def initialize @a = 'foo' @@ -1194,7 +1324,7 @@ assert_equal 'true', %q{ } Ractor.shareable?(a) -} +RUBY # Ractor.make_shareable(obj) doesn't freeze shareable objects assert_equal 'true', %q{ @@ -1206,9 +1336,13 @@ assert_equal 'true', %q{ # Ractor.make_shareable(a_proc) makes a proc shareable. assert_equal 'true', %q{ a = [1, [2, 3], {a: "4"}] - pr = Proc.new do - a + + 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) @@ -1256,10 +1390,12 @@ assert_equal '1', %q{ # 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 + 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 @@ -1285,14 +1421,14 @@ assert_equal '[false, false, true, true]', %q{ } # TracePoint with normal Proc should be Ractor local -assert_equal '[4, 8]', %q{ +assert_equal '[6, 10]', %q{ rs = [] TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do - Ractor.new{ # line 4 + Ractor.new{ # line 5 a = 1 b = 2 }.take - c = 3 # line 8 + c = 3 # line 9 end rs } @@ -1360,28 +1496,13 @@ assert_equal "#{N}#{N}", %Q{ }.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 = +'' obj.instance_variable_set("@a", 1) obj.instance_variable_set("@b", 1) obj.instance_variable_set("@c", 1) @@ -1393,8 +1514,9 @@ assert_equal "#{n}#{n}", %Q{ # NameError assert_equal "ok", %q{ + obj = "".freeze # NameError refers the receiver indirectly begin - bar + obj.bar rescue => err end begin @@ -1404,4 +1526,280 @@ assert_equal "ok", %q{ 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) +} |