From c92d47251c96a9eec89b48882679078e8d179e8a Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 11 Jul 2024 07:01:29 +0900 Subject: [PATCH] `require` on Ractors Some of required libraries need to run on main Ractor because of setting constants with unshareable objects and so on. So this patch allows `require` by running `require` on the main ractor by `rb_ractor_interrupt_exec()` (which makes a thread in the specified racotr and run passed C-func on the created thread). If it failed, the exception object will be (copy) sent from the main ractor to the caller ractor and raises on it. If the transfer failed (maybe because the exception object is not copy-able), tell the failure of transfering. Same on `require_relative` and `autoload`. --- bootstraptest/test_ractor.rb | 88 ++++++++++++++-- ext/io/console/console.c | 2 + load.c | 23 ++-- ractor.c | 185 +++++++++++++++++++++++++++++++++ ractor.rb | 27 +++++ ractor_core.h | 2 + test/-ext-/symbol/test_type.rb | 1 + variable.c | 2 +- 8 files changed, 311 insertions(+), 19 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 0390d38f9c3408..d5120a22a6ef9b 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -211,17 +211,6 @@ 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 @@ -1822,3 +1811,80 @@ class C8; def self.foo = 17; end shareable = Ractor.make_shareable("chilled") shareable == "chilled" && Ractor.shareable?(shareable) } + +# require in Ractor +assert_equal 'true', %q{ + Module.new do + def require feature + return Ractor.require(feature) unless Ractor.main? + super + end + Object.prepend self + set_temporary_name 'Ractor#require' + end + + Ractor.new{ + require 'benchmark' + Benchmark.measure{} + }.take.real > 0 +} + +# require_relative in Ractor +assert_equal 'true', %q{ + dummyfile = File.join(__dir__, "dummy#{rand}.rb") + return true if File.exist?(dummyfile) + + begin + File.write dummyfile, '' + rescue Exception + # skip on any errors + return true + end + + begin + Ractor.new dummyfile do |f| + require_relative File.basename(f) + end.take + ensure + File.unlink dummyfile + end +} + +# require_relative in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + return true if File.exist?(dummyfile) + + Ractor.new dummyfile do |f| + begin + require_relative File.basename(f) + rescue LoadError => e + e.class + end + end.take +} + +# autolaod in Ractor +assert_equal 'true', %q{ + autoload :Benchmark, 'benchmark' + + r = Ractor.new do + Benchmark.measure{} + end + r.take.real > 0 +} + +# failed in autolaod in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + autoload :Benchmark, dummyfile + + r = Ractor.new do + begin + Benchmark.measure{} + rescue LoadError => e + e.class + end + end + r.take +} diff --git a/ext/io/console/console.c b/ext/io/console/console.c index d88184ec9d1dea..d9a48a519455a6 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1786,6 +1786,8 @@ Init_console(void) void InitVM_console(void) { + rb_ext_ractor_safe(true); + rb_define_method(rb_cIO, "raw", console_raw, -1); rb_define_method(rb_cIO, "raw!", console_set_raw, -1); rb_define_method(rb_cIO, "cooked", console_cooked, 0); diff --git a/load.c b/load.c index a1729c2ba10cbb..5c5e0592bc9097 100644 --- a/load.c +++ b/load.c @@ -18,6 +18,7 @@ #include "darray.h" #include "ruby/encoding.h" #include "ruby/util.h" +#include "ractor_core.h" static VALUE ruby_dln_libmap; @@ -1383,17 +1384,25 @@ static VALUE rb_require_string_internal(VALUE fname, bool resurrect) { rb_execution_context_t *ec = GET_EC(); - int result = require_internal(ec, fname, 1, RTEST(ruby_verbose)); - if (result > TAG_RETURN) { - EC_JUMP_TAG(ec, result); - } - if (result < 0) { + // main ractor check + if (!rb_ractor_main_p()) { if (resurrect) fname = rb_str_resurrect(fname); - load_failed(fname); + return rb_ractor_require(fname); } + else { + int result = require_internal(ec, fname, 1, RTEST(ruby_verbose)); + + if (result > TAG_RETURN) { + EC_JUMP_TAG(ec, result); + } + if (result < 0) { + if (resurrect) fname = rb_str_resurrect(fname); + load_failed(fname); + } - return RBOOL(result); + return RBOOL(result); + } } VALUE diff --git a/ractor.c b/ractor.c index 2323f6b9eb0039..e5d6931086f94b 100644 --- a/ractor.c +++ b/ractor.c @@ -3863,4 +3863,189 @@ ractor_local_value_set(rb_execution_context_t *ec, VALUE self, VALUE sym, VALUE return val; } +// Ractor::Channel (emulate with Ractor) + +typedef rb_ractor_t rb_ractor_channel_t; + +static VALUE +rb_ractor_channel_new(void) +{ + return rb_funcall(rb_const_get(rb_cRactor, rb_intern("Channel")), rb_intern("new"), 0); +} + +static VALUE +rb_ractor_channel_yield(VALUE vch, VALUE obj) +{ + rb_ractor_channel_t *ch = RACTOR_PTR(vch); + ractor_send(GET_EC(), (rb_ractor_t *)ch, obj, Qfalse); + return Qnil; +} + +static VALUE +rb_ractor_channel_take(VALUE vch) +{ + rb_ractor_channel_t *ch = RACTOR_PTR(vch); + return ractor_take(GET_EC(), (rb_ractor_t *)ch); +} + +static VALUE +rb_ractor_channel_close(VALUE vch) +{ + rb_ractor_channel_t *ch = RACTOR_PTR(vch); + return ractor_close_incoming(GET_EC(), (rb_ractor_t *)ch); +} + +// Ractor#require + +struct cross_ractor_require { + VALUE ch; + VALUE result; + VALUE exception; + + // require + VALUE feature; + + // autoload + VALUE module; + ID name; +}; + +static VALUE +require_body(VALUE data) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + + ID require; + CONST_ID(require, "require"); + crr->result = rb_funcallv(Qnil, require, 1, &crr->feature); + + return Qnil; +} + +static VALUE +require_rescue(VALUE data, VALUE errinfo) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + crr->exception = errinfo; + return Qundef; +} + +static VALUE +require_result_copy_body(VALUE data) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + + if (crr->exception != Qundef) { + VM_ASSERT(crr->result == Qnil); + crr->exception = ractor_copy(crr->exception); + } + else{ + VM_ASSERT(crr->result != Qundef); + crr->result = ractor_copy(crr->result); + } + + return Qnil; +} + +static VALUE +require_result_copy_resuce(VALUE data, VALUE errinfo) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + crr->exception = errinfo; // ractor_move(crr->exception); + return Qnil; +} + +static VALUE +ractor_require_protect(struct cross_ractor_require *crr, VALUE (*func)(VALUE)) +{ + // catch any error + rb_rescue2(func, (VALUE)crr, + require_rescue, (VALUE)crr, rb_eException, 0); + + rb_rescue2(require_result_copy_body, (VALUE)crr, + require_result_copy_resuce, (VALUE)crr, rb_eException, 0); + + rb_ractor_channel_yield(crr->ch, Qtrue); + return Qnil; + +} + +static VALUE +ractore_require_func(void *data) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + return ractor_require_protect(crr, require_body); +} + +VALUE +rb_ractor_require(VALUE feature) +{ + // TODO: make feature shareable + struct cross_ractor_require crr = { + .feature = feature, // TODO: ractor + .ch = rb_ractor_channel_new(), + .result = Qundef, + .exception = Qundef, + }; + + rb_ractor_t *r = GET_VM()->ractor.main_ractor; + rb_ractor_interrupt_exec(r, ractore_require_func, &crr, 0); + + // wait for require done + rb_ractor_channel_take(crr.ch); + + if (crr.exception != Qundef) { + rb_exc_raise(crr.exception); + } + else { + return crr.result; + } +} + +static VALUE +ractor_require(rb_execution_context_t *ec, VALUE self, VALUE feature) +{ + return rb_ractor_require(feature); +} + +static VALUE +autoload_load_body(VALUE data) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + crr->result = rb_autoload_load(crr->module, crr->name); + return Qnil; +} + +static VALUE +ractor_autoload_load_func(void *data) +{ + struct cross_ractor_require *crr = (struct cross_ractor_require *)data; + return ractor_require_protect(crr, autoload_load_body); +} + +VALUE +rb_ractor_autoload_load(VALUE module, ID name) +{ + struct cross_ractor_require crr = { + .module = module, + .name = name, + .ch = rb_ractor_channel_new(), + .result = Qundef, + .exception = Qundef, + }; + + rb_ractor_t *r = GET_VM()->ractor.main_ractor; + rb_ractor_interrupt_exec(r, ractor_autoload_load_func, &crr, 0); + + // wait for require done + rb_ractor_channel_take(crr.ch); + + if (crr.exception != Qundef) { + rb_exc_raise(crr.exception); + } + else { + return crr.result; + } +} + #include "ractor.rbinc" diff --git a/ractor.rb b/ractor.rb index d18062ca079919..b957b2b9999da4 100644 --- a/ractor.rb +++ b/ractor.rb @@ -844,10 +844,37 @@ def []=(sym, val) Primitive.ractor_local_value_set(sym, val) end + class Channel + def self.new + Ractor.new do + while true + obj = Ractor.receive + Ractor.yield obj + end + rescue Ractor::ClosedError + nil + end + end + end + # returns main ractor def self.main __builtin_cexpr! %q{ rb_ractor_self(GET_VM()->ractor.main_ractor); } end + + def self.main? + __builtin_cexpr! %q{ + GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec) + } + end + + def self.require feature + if main? + super feature + else + Primitive.ractor_require feature + end + end end diff --git a/ractor_core.h b/ractor_core.h index e35be2f21c10d8..315c3ecab610f5 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -228,6 +228,8 @@ void rb_ractor_terminate_interrupt_main_thread(rb_ractor_t *r); void rb_ractor_terminate_all(void); bool rb_ractor_main_p_(void); void rb_ractor_atfork(rb_vm_t *vm, rb_thread_t *th); +VALUE rb_ractor_require(VALUE feature); +VALUE rb_ractor_autoload_load(VALUE space, ID id); VALUE rb_ractor_ensure_shareable(VALUE obj, VALUE name); diff --git a/test/-ext-/symbol/test_type.rb b/test/-ext-/symbol/test_type.rb index 2b0fbe5b798b93..8029787bdd2385 100644 --- a/test/-ext-/symbol/test_type.rb +++ b/test/-ext-/symbol/test_type.rb @@ -1,4 +1,5 @@ # frozen_string_literal: false + require 'test/unit' require "-test-/symbol" diff --git a/variable.c b/variable.c index 8be3d9fdb5dfa2..fa065cad78e14d 100644 --- a/variable.c +++ b/variable.c @@ -2997,7 +2997,7 @@ rb_autoload_load(VALUE module, ID name) // At this point, we assume there might be autoloading, so fail if it's ractor: if (UNLIKELY(!rb_ractor_main_p())) { - rb_raise(rb_eRactorUnsafeError, "require by autoload on non-main Ractor is not supported (%s)", rb_id2name(name)); + return rb_ractor_autoload_load(module, name); } // This state is stored on the stack and is used during the autoload process.