support `require` in non-main Ractors

Many libraries should be loaded on the main ractor because of
setting constants with unshareable objects and so on.

This patch allows to call `requore` on non-main Ractors by
asking the main ractor to call `require` on it. The calling ractor
waits for the result of `require` from the main ractor.

If the `require` call failed with some reasons, an exception
objects will be deliverred from the main ractor to the calling ractor
if it is copy-able.

Same on `require_relative` and `require` by `autoload`.

Now `Ractor.new{pp obj}` works well (the first call of `pp` requires
`pp` library implicitly).

[Feature #20627]
This commit is contained in:
Koichi Sasada 2024-11-05 04:54:06 +09:00
Родитель 075a102c93
Коммит aa63699d10
10 изменённых файлов: 388 добавлений и 24 удалений

Просмотреть файл

@ -211,17 +211,6 @@ assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{
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 @@ assert_equal 'true', %q{
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
}

23
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));
return RBOOL(result);
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);
}
}
VALUE

248
ractor.c
Просмотреть файл

@ -1956,6 +1956,7 @@ cancel_single_ractor_mode(void)
}
ruby_single_main_ractor = NULL;
rb_funcall(rb_cRactor, rb_intern("_activated"), 0);
}
static void
@ -2136,6 +2137,13 @@ ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VAL
return rv;
}
static VALUE
ractor_create_func(VALUE klass, VALUE loc, VALUE name, VALUE args, rb_block_call_func_t func)
{
VALUE block = rb_proc_new(func, Qnil);
return ractor_create(rb_current_ec_noinline(), klass, loc, name, args, block);
}
static void
ractor_yield_atexit(rb_execution_context_t *ec, rb_ractor_t *cr, VALUE v, bool exc)
{
@ -2665,6 +2673,8 @@ Init_Ractor(void)
rb_define_method(rb_cRactorMovedObject, "instance_eval", ractor_moved_missing, -1);
rb_define_method(rb_cRactorMovedObject, "instance_exec", ractor_moved_missing, -1);
// internal
#if USE_RACTOR_SELECTOR
rb_init_ractor_selector();
#endif
@ -3869,4 +3879,242 @@ 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
ractor_channel_func(RB_BLOCK_CALL_FUNC_ARGLIST(y, c))
{
rb_execution_context_t *ec = GET_EC();
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
while (1) {
int state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
VALUE obj = ractor_receive(ec, cr);
ractor_yield(ec, cr, obj, Qfalse);
}
EC_POP_TAG();
if (state) {
// ignore the error
break;
}
}
return Qnil;
}
static VALUE
rb_ractor_channel_new(void)
{
#if 0
return rb_funcall(rb_const_get(rb_cRactor, rb_intern("Channel")), rb_intern("new"), 0);
#else
// class Channel
// def self.new
// Ractor.new do # func body
// while true
// obj = Ractor.receive
// Ractor.yield obj
// end
// rescue Ractor::ClosedError
// nil
// end
// end
// end
return ractor_create_func(rb_cRactor, Qnil, rb_str_new2("Ractor/channel"), rb_ary_new(), ractor_channel_func);
#endif
}
static VALUE
rb_ractor_channel_yield(rb_execution_context_t *ec, VALUE vch, VALUE obj)
{
VM_ASSERT(ec == rb_current_ec_noinline());
rb_ractor_channel_t *ch = RACTOR_PTR(vch);
ractor_send(ec, (rb_ractor_t *)ch, obj, Qfalse);
return Qnil;
}
static VALUE
rb_ractor_channel_take(rb_execution_context_t *ec, VALUE vch)
{
VM_ASSERT(ec == rb_current_ec_noinline());
rb_ractor_channel_t *ch = RACTOR_PTR(vch);
return ractor_take(ec, (rb_ractor_t *)ch);
}
static VALUE
rb_ractor_channel_close(rb_execution_context_t *ec, VALUE vch)
{
VM_ASSERT(ec == rb_current_ec_noinline());
rb_ractor_channel_t *ch = RACTOR_PTR(vch);
ractor_close_incoming(ec, (rb_ractor_t *)ch);
return ractor_close_outgoing(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 == Qundef);
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(GET_EC(), 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_execution_context_t *ec = GET_EC();
rb_ractor_t *main_r = GET_VM()->ractor.main_ractor;
rb_ractor_interrupt_exec(main_r, ractore_require_func, &crr, 0);
// wait for require done
rb_ractor_channel_take(ec, crr.ch);
rb_ractor_channel_close(ec, 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_execution_context_t *ec = GET_EC();
rb_ractor_t *main_r = GET_VM()->ractor.main_ractor;
rb_ractor_interrupt_exec(main_r, ractor_autoload_load_func, &crr, 0);
// wait for require done
rb_ractor_channel_take(ec, crr.ch);
rb_ractor_channel_close(ec, crr.ch);
if (crr.exception != Qundef) {
rb_exc_raise(crr.exception);
}
else {
return crr.result;
}
}
#include "ractor.rbinc"

Просмотреть файл

@ -835,11 +835,13 @@ class Ractor
end
# get a value from ractor-local storage of current Ractor
# Obsolete and use Ractor.[] instead.
def [](sym)
Primitive.ractor_local_value(sym)
end
# set a value in ractor-local storage of current Ractor
# Obsolete and use Ractor.[]= instead.
def []=(sym, val)
Primitive.ractor_local_value_set(sym, val)
end
@ -867,4 +869,32 @@ class Ractor
GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec)
}
end
# internal method
def self._require feature
if main?
super feature
else
Primitive.ractor_require feature
end
end
class << self
private
# internal method that is called when the first "Ractor.new" is called
def _activated
Kernel.prepend Module.new{|m|
m.set_temporary_name '<RactorRequire>'
def require feature
if Ractor.main?
super
else
Ractor._require feature
end
end
}
end
end
end

Просмотреть файл

@ -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);

Просмотреть файл

@ -845,7 +845,7 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b
RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
ASSERT_thread_sched_locked(sched, th);
VM_ASSERT(th == GET_THREAD());
VM_ASSERT(th == rb_ec_thread_ptr(rb_current_ec_noinline()));
if (th != sched->running) {
// already deleted from running threads
@ -900,12 +900,12 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b
thread_sched_set_lock_owner(sched, th);
}
VM_ASSERT(GET_EC() == th->ec);
VM_ASSERT(rb_current_ec_noinline() == th->ec);
}
}
VM_ASSERT(th->nt != NULL);
VM_ASSERT(GET_EC() == th->ec);
VM_ASSERT(rb_current_ec_noinline() == th->ec);
VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
// add th to running threads

Просмотреть файл

@ -132,7 +132,6 @@ struct rb_thread_sched {
#ifdef RB_THREAD_LOCAL_SPECIFIER
NOINLINE(void rb_current_ec_set(struct rb_execution_context_struct *));
NOINLINE(struct rb_execution_context_struct *rb_current_ec_noinline(void));
# ifdef __APPLE__
// on Darwin, TLS can not be accessed across .so

Просмотреть файл

@ -2994,7 +2994,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.

10
vm.c
Просмотреть файл

@ -556,7 +556,7 @@ RB_THREAD_LOCAL_SPECIFIER rb_execution_context_t *ruby_current_ec;
RB_THREAD_LOCAL_SPECIFIER rb_atomic_t ruby_nt_serial;
#endif
// no-inline decl on thread_pthread.h
// no-inline decl on vm_core.h
rb_execution_context_t *
rb_current_ec_noinline(void)
{
@ -580,6 +580,14 @@ rb_current_ec(void)
#endif
#else
native_tls_key_t ruby_current_ec_key;
// no-inline decl on vm_core.h
rb_execution_context_t *
rb_current_ec_noinline(void)
{
return native_tls_get(ruby_current_ec_key);
}
#endif
rb_event_flag_t ruby_vm_event_flags;

Просмотреть файл

@ -1969,6 +1969,8 @@ rb_ec_vm_ptr(const rb_execution_context_t *ec)
}
}
NOINLINE(struct rb_execution_context_struct *rb_current_ec_noinline(void));
static inline rb_execution_context_t *
rb_current_execution_context(bool expect_ec)
{