From 3aee4fd6c59f721c221e74cd2bf39aa5a254abce 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 b6a4ef605e6233..941a50feb40e96 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 @@ -1836,3 +1825,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 85e6a0613eed59..7c970b2828e086 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1846,6 +1846,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 c6fb43e9e860d5..ad33483a4d0aa3 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 9d34dd1b7d269a..53e313eddb7c69 100644 --- a/ractor.c +++ b/ractor.c @@ -3868,4 +3868,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 fd3cba3a1f409b..c5ca883d3151ca 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -222,6 +222,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 0dfc5cb7b83dfc..c30557491956e1 100644 --- a/variable.c +++ b/variable.c @@ -2991,7 +2991,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.