Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions error.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ static ID id_deprecated;
static ID id_experimental;
static ID id_performance;
static ID id_strict_unused_block;
static ID id_ractor_isolation;
static VALUE sym_category;
static VALUE sym_highlight;
static struct {
Expand Down Expand Up @@ -224,6 +225,10 @@ rb_warning_category_enabled_p(rb_warning_category_t category)
* +:performance+ ::
* performance hints
* * Shape variation limit
*
* +:ractor_isolation+ ::
* Ractor isolation violations reported under RUBY_RACTOR_CHECK_ISOLATION
* (downgraded from Ractor::IsolationError exceptions to warnings).
*/

static VALUE
Expand Down Expand Up @@ -3884,6 +3889,7 @@ Init_Exception(void)
id_experimental = rb_intern_const("experimental");
id_performance = rb_intern_const("performance");
id_strict_unused_block = rb_intern_const("strict_unused_block");
id_ractor_isolation = rb_intern_const("ractor_isolation");
id_top = rb_intern_const("top");
id_bottom = rb_intern_const("bottom");
id_iseq = rb_make_internal_id();
Expand All @@ -3897,13 +3903,15 @@ Init_Exception(void)
st_add_direct(warning_categories.id2enum, id_experimental, RB_WARN_CATEGORY_EXPERIMENTAL);
st_add_direct(warning_categories.id2enum, id_performance, RB_WARN_CATEGORY_PERFORMANCE);
st_add_direct(warning_categories.id2enum, id_strict_unused_block, RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK);
st_add_direct(warning_categories.id2enum, id_ractor_isolation, RB_WARN_CATEGORY_RACTOR_ISOLATION);

warning_categories.enum2id = rb_init_identtable();
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_NONE, 0);
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_DEPRECATED, id_deprecated);
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_EXPERIMENTAL, id_experimental);
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_PERFORMANCE, id_performance);
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK, id_strict_unused_block);
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_RACTOR_ISOLATION, id_ractor_isolation);
}

void
Expand Down
14 changes: 13 additions & 1 deletion gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,12 @@ rb_undefine_finalizer(VALUE obj)
{
rb_check_frozen(obj);

if (rb_gc_obj_foreign_p(obj)) {
rb_ractor_isolation_violation(
"can not undefine a finalizer of an object of another Ractor");
return obj;
}

rb_gc_impl_undefine_finalizer(rb_gc_get_objspace(), obj);

return obj;
Expand Down Expand Up @@ -2217,7 +2223,13 @@ rb_define_finalizer(VALUE obj, VALUE block)
should_be_finalizable(obj);
should_be_callable(block);

block = rb_gc_impl_define_finalizer(rb_gc_get_objspace(), obj, block);
if (rb_gc_obj_foreign_p(obj)) {
rb_ractor_isolation_violation(
"can not define a finalizer for an object of another Ractor");
}
else {
block = rb_gc_impl_define_finalizer(rb_gc_get_objspace(), obj, block);
}

block = rb_ary_new3(2, INT2FIX(0), block);
OBJ_FREEZE(block);
Expand Down
5 changes: 5 additions & 0 deletions include/ruby/internal/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,21 @@ typedef enum {
/** Warning is for checking unused block strictly */
RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK,

/** Warning is for Ractor isolation violations reported under RUBY_RACTOR_CHECK_ISOLATION. */
RB_WARN_CATEGORY_RACTOR_ISOLATION,

RB_WARN_CATEGORY_DEFAULT_BITS = (
(1U << RB_WARN_CATEGORY_DEPRECATED) |
(1U << RB_WARN_CATEGORY_EXPERIMENTAL) |
(1U << RB_WARN_CATEGORY_RACTOR_ISOLATION) |
0),

RB_WARN_CATEGORY_ALL_BITS = (
(1U << RB_WARN_CATEGORY_DEPRECATED) |
(1U << RB_WARN_CATEGORY_EXPERIMENTAL) |
(1U << RB_WARN_CATEGORY_PERFORMANCE) |
(1U << RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK) |
(1U << RB_WARN_CATEGORY_RACTOR_ISOLATION) |
0)
} rb_warning_category_t;

Expand Down
8 changes: 7 additions & 1 deletion process.c
Original file line number Diff line number Diff line change
Expand Up @@ -4109,7 +4109,13 @@ rb_pid_t
rb_fork_ruby(int *status)
{
if (UNLIKELY(!rb_ractor_main_p())) {
rb_raise(rb_eRactorIsolationError, "can not fork from non-main Ractors");
rb_ractor_isolation_violation("can not fork from non-main Ractors");

/* Reached only when the violation warned instead of raising. fork keeps
* just the calling thread, so refuse it rather than hand the child a VM
* whose other Ractors are gone; every caller already handles -1. */
errno = EPERM;
return -1;
}

struct rb_process_status child = {.status = 0};
Expand Down
128 changes: 117 additions & 11 deletions ractor.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ruby/ractor.h"
#include "ruby/re.h"
#include "ruby/thread_native.h"
#include "ruby_atomic.h"
#include "vm_core.h"
#include "vm_sync.h"
#include "ractor_core.h"
Expand Down Expand Up @@ -1850,6 +1851,12 @@ make_shareable_check_shareable(VALUE obj)
}
else if (!allow_frozen_shareable_p(obj)) {
if (!RB_TYPE_P(obj, T_DATA)) {
if (rb_ractor_isolation_check_p()) {
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
"can not make shareable object of class %+"PRIsVALUE,
rb_class_of(obj));
return traverse_stop;
}
rb_raise(rb_eRactorError,
"can not make shareable object for %+"PRIsVALUE, obj);
}
Expand All @@ -1859,14 +1866,26 @@ make_shareable_check_shareable(VALUE obj)
RB_OBJ_SET_SHAREABLE(obj);
return traverse_skip;
}
else if (rb_ractor_isolation_check_p()) {
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
"can not make shareable object of class %+"PRIsVALUE
" because it refers unshareable objects", rb_class_of(obj));
return traverse_stop;
}
else {
rb_raise(rb_eRactorError,
"can not make shareable object for %+"PRIsVALUE" because it refers unshareable objects", obj);
}
}
else if (rb_obj_is_proc(obj)) {
rb_proc_ractor_make_shareable(obj, Qundef);
return traverse_cont;
return rb_ractor_shareable_p(obj) ? traverse_cont : traverse_stop;
}
else if (rb_ractor_isolation_check_p()) {
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
"can not make shareable object of class %+"PRIsVALUE,
rb_class_of(obj));
return traverse_stop;
}
else {
rb_raise(rb_eRactorError, "can not make shareable object for %+"PRIsVALUE, obj);
Expand Down Expand Up @@ -1930,9 +1949,10 @@ VALUE
rb_ractor_ensure_shareable(VALUE obj, VALUE name)
{
if (!rb_ractor_shareable_p(obj)) {
VALUE message = rb_sprintf("cannot assign unshareable object to %"PRIsVALUE,
name);
rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, message));
rb_ractor_isolation_violation("cannot assign unshareable object to %"PRIsVALUE, name);
// In isolation-check mode the violation only warned: return obj as-is
// so the caller can keep going. The caller's invariant ("this is now
// shareable") will be wrong, which is exactly the bug we want surfaced.
}
return obj;
}
Expand All @@ -1941,7 +1961,7 @@ void
rb_ractor_ensure_main_ractor(const char *msg)
{
if (!rb_ractor_main_p()) {
rb_raise(rb_eRactorIsolationError, "%s", msg);
rb_ractor_isolation_violation("%s", msg);
}
}

Expand Down Expand Up @@ -3783,13 +3803,12 @@ ractor_local_value_store_if_absent(rb_execution_context_t *ec, VALUE self, VALUE
static VALUE
ractor_shareable_proc(rb_execution_context_t *ec, VALUE replace_self, bool is_lambda)
{
if (!rb_ractor_shareable_p(replace_self)) {
rb_raise(rb_eRactorIsolationError, "self should be shareable: %" PRIsVALUE, replace_self);
}
else {
VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc();
return rb_proc_ractor_make_shareable(rb_proc_dup(proc), replace_self);
// in check mode, rb_proc_ractor_make_shareable below reports this violation
if (!rb_ractor_shareable_p(replace_self) && !rb_ractor_isolation_check_p()) {
rb_ractor_isolation_violation("self should be shareable: %" PRIsVALUE, replace_self);
}
VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc();
return rb_proc_ractor_make_shareable(rb_proc_dup(proc), replace_self);
}

// Ractor#require
Expand Down Expand Up @@ -4001,4 +4020,91 @@ rb_ractor_autoload_load(VALUE module, ID name)
}
}

// =============================================================================
// RUBY_RACTOR_CHECK_ISOLATION (environment variable, read once at boot)
//
// A development/debugging mode: isolation violations on non-main Ractors are
// downgraded from Ractor::IsolationError to :ractor_isolation category
// warnings so the program can keep running and report more than the first
// violation. The main Ractor is unaffected and keeps raising as usual.
//
// The mode only changes how violations are reported. Creating a Ractor still
// switches the VM into multi-ractor mode (ordinary Ractor.new semantics).
// Multi-ractor mode cannot be turned off again, so the VM keeps paying that
// overhead for the rest of the process lifetime.
// =============================================================================

/* Set at boot from the environment; see thread_sched.c and version.c. */
extern int ruby_ractor_check_isolation_enabled;

bool
rb_ractor_isolation_check_p(void)
{
if (!ruby_ractor_check_isolation_enabled) return false;
rb_execution_context_t *ec = rb_current_ec_noinline();
if (!ec) return false;
rb_ractor_t *r = rb_ec_ractor_ptr(ec);
return r && r != rb_ec_vm_ptr(ec)->ractor.main_ractor;
}

void
rb_ractor_isolation_violation_str(VALUE message)
{
if (rb_ractor_isolation_check_p()) {
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION, "%s", StringValueCStr(message));
return;
}

rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, message));
}

// One check-mode warning per (C site, Ruby site); keys are malloc'd, the table is VM-global.
static st_table *isolation_warn_tbl;
static unsigned long isolation_warn_suppressed;

static bool
isolation_warn_first_p(const char *fmt)
{
int line = 0;
const char *file = rb_source_location_cstr(&line);
VALUE loc = rb_sprintf("%p:%s:%d", (const void *)fmt, file ? file : "-", line);
char *key = strdup(RSTRING_PTR(loc));
if (!key) return true;

bool first;
RB_VM_LOCKING() {
if (!isolation_warn_tbl) isolation_warn_tbl = st_init_strtable();
first = !st_insert(isolation_warn_tbl, (st_data_t)key, 0);
if (!first) isolation_warn_suppressed++;
}
if (!first) free(key);
return first;
}

void
rb_ractor_isolation_warning_summary(void)
{
if (isolation_warn_suppressed) {
fprintf(stderr, "RUBY_RACTOR_CHECK_ISOLATION: %lu repeated isolation warnings suppressed\n",
isolation_warn_suppressed);
}
}

void
rb_ractor_isolation_violation(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
VALUE message = rb_vsprintf(fmt, args);
va_end(args);

if (rb_ractor_isolation_check_p()
&& (NIL_P(ruby_verbose) || !rb_warning_category_enabled_p(RB_WARN_CATEGORY_RACTOR_ISOLATION)
|| !isolation_warn_first_p(fmt))) {
return;
}

rb_ractor_isolation_violation_str(message);
}

#include "ractor.rbinc"
17 changes: 17 additions & 0 deletions ractor_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,23 @@ VALUE rb_ractor_autoload_load(VALUE space, ID id);
VALUE rb_ractor_ensure_shareable(VALUE obj, VALUE name);
st_table *rb_ractor_targeted_hooks(rb_ractor_t *cr);

/* True if RUBY_RACTOR_CHECK_ISOLATION mode is enabled and the current Ractor
* is a non-main Ractor. */
bool rb_ractor_isolation_check_p(void);

/* Report a Ractor isolation violation:
* - if RUBY_RACTOR_CHECK_ISOLATION mode is enabled and the current Ractor
* is a non-main Ractor, emit a :ractor_isolation category warning and
* return;
* - otherwise, raise Ractor::IsolationError (does not return).
*
* Use the printf-style overload for ad-hoc messages and the _str overload
* when the message is already constructed (e.g. via several rb_str_catf
* calls). */
PRINTF_ARGS(void rb_ractor_isolation_violation(const char *fmt, ...), 1, 2);
void rb_ractor_isolation_violation_str(VALUE message);
void rb_ractor_isolation_warning_summary(void);

RUBY_SYMBOL_EXPORT_BEGIN
void rb_ractor_finish_marking(bool full_mark);

Expand Down
14 changes: 14 additions & 0 deletions ractor_sync.c
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,20 @@ ractor_prepare_payload(rb_execution_context_t *ec, VALUE obj, enum ractor_basket
*ptype = basket_type_ref;
return obj;
}
else if (rb_ractor_isolation_check_p()) {
// Under RUBY_RACTOR_CHECK_ISOLATION, don't copy non-shareable messages.
// Copying can fail outright (e.g. Procs -> "can not copy Proc
// object"), which would abort a real-Ractor sweep at the first
// Ractor::Dispatch call. Exclusive mode (RUBY_RACTOR_EXCLUSIVE)
// guarantees no other Ractor runs concurrently, so passing the
// original object by reference is safe; warn and continue.
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
"can not copy an unshareable %"PRIsVALUE" across Ractors; "
"passing by reference under RUBY_RACTOR_CHECK_ISOLATION",
rb_class_of(obj));
*ptype = basket_type_ref;
return obj;
}
else {
/* Snapshot the object on the sender side without calling the user-visible
* #clone. Both forms are off-heap, so an in-flight payload is never a GC
Expand Down
4 changes: 4 additions & 0 deletions ruby.c
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ usage(const char *name, int help, int highlight, int columns)
M("experimental", "", "Experimental features."),
M("performance", "", "Performance issues."),
M("strict_unused_block", "", "Warning unused block strictly"),
M("ractor_isolation", "", "Ractor isolation violations."),
};
int i;
const char *sb = highlight ? esc_standout+1 : esc_none;
Expand Down Expand Up @@ -1270,6 +1271,9 @@ proc_W_option(ruby_cmdline_options_t *opt, const char *s, int *warning)
else if (NAME_MATCH_P("strict_unused_block", s, len)) {
bits = 1U << RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK;
}
else if (NAME_MATCH_P("ractor_isolation", s, len)) {
bits = 1U << RB_WARN_CATEGORY_RACTOR_ISOLATION;
}
else {
rb_warn("unknown warning category: '%s'", s);
}
Expand Down
Loading
Loading