Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1691eab
Add Ractor.check_isolation
skipkayhil Sep 1, 2026
62e1dd1
Replace Ractor.check_isolation with RUBY_RACTOR_CHECK_ISOLATION
yaroslav-shopify Sep 18, 2026
1ee685a
Do not fork from a non-main Ractor under RUBY_RACTOR_CHECK_ISOLATION
yaroslav-shopify Sep 18, 2026
0d5add4
Fix boot-advisory test on builds without M:N threads
yaroslav-shopify Sep 18, 2026
a4cc237
Explain check-mode guard in ractor_shareable_proc
yaroslav-shopify Sep 18, 2026
9b136f3
Print the check-isolation advisory even under -W0
yaroslav-shopify Sep 18, 2026
3463786
Deduplicate check-mode isolation warnings by call site
yaroslav-shopify Sep 18, 2026
8a6f7a5
Serialize Ractors under RUBY_RACTOR_CHECK_ISOLATION, drop RUBY_RACTOR…
yaroslav-shopify Sep 21, 2026
3b0fea3
Trim check-isolation comments to what the code cannot say
yaroslav-shopify Sep 21, 2026
a18df5c
Merge branch 'master' into ractor-check-isolation
yaroslav-shopify Sep 21, 2026
0e9d95d
Skip the debug belonging assertion under RUBY_RACTOR_CHECK_ISOLATION
yaroslav-shopify Sep 21, 2026
22e095f
Add RUBY_RACTOR_CHECK_ISOLATION=2 to report every violation
yaroslav-shopify Sep 21, 2026
79a2e96
Run every GC as a global cycle under RUBY_RACTOR_CHECK_ISOLATION
yaroslav-shopify Sep 21, 2026
4a251b6
Deduplicate and locate isolation warnings raised inside internal frames
yaroslav-shopify Sep 21, 2026
9a2119f
Rename RUBY_RACTOR_CHECK_ISOLATION to RUBY_RACTOR_ISOLATION
yaroslav-shopify Sep 22, 2026
fd52ef5
Reduce isolation warning allocations and reuse Ractor helpers
yaroslav-shopify Sep 22, 2026
8a7477d
Prevent recursive isolation warnings and unify reporting
yaroslav-shopify Sep 22, 2026
4b54196
Fix isolation diagnostics for classes, Procs and constants
yaroslav-shopify Sep 22, 2026
adf5510
Simplify RUBY_RACTOR_ISOLATION code for readability
yaroslav-shopify Sep 22, 2026
4ecb1db
Address code review by Luke Gruber
yaroslav-shopify Sep 23, 2026
60d3075
Fix isolation warning test on builds without M:N threading
yaroslav-shopify Sep 23, 2026
8fa52c3
Merge branch 'master' into ractor-check-isolation
yaroslav-shopify Sep 29, 2026
fcb05ee
Run explicit GC.start globally under RUBY_RACTOR_ISOLATION
yaroslav-shopify Sep 30, 2026
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
20 changes: 9 additions & 11 deletions class.c
Original file line number Diff line number Diff line change
Expand Up @@ -686,9 +686,8 @@ void
rb_class_owner_check(VALUE klass)
{
if (UNLIKELY(!rb_class_owned_p(klass))) {
rb_raise(rb_eRactorIsolationError,
"can not modify %"PRIsVALUE" because it is created by another Ractor",
class_owner_name(klass));
rb_ractor_isolation_violation("can not modify %"PRIsVALUE" because it is created by another Ractor",
class_owner_name(klass));
}
}

Expand Down Expand Up @@ -1009,9 +1008,9 @@ init_copy_check_const_i(ID id, VALUE v, void *data)
{
const rb_const_entry_t *ce = (const rb_const_entry_t *)v;
if (!UNDEF_P(ce->value) && !rb_ractor_shareable_p(ce->value)) {
rb_raise(rb_eRactorIsolationError,
"can not copy a class/module created by another Ractor because "
"constant %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
rb_ractor_isolation_violation(
"can not copy a class/module created by another Ractor because "
"constant %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
}
return ID_TABLE_CONTINUE;
}
Expand All @@ -1020,9 +1019,9 @@ static int
init_copy_check_field_i(ID id, VALUE val, st_data_t arg)
{
if ((rb_is_instance_id(id) || rb_is_class_id(id)) && !rb_ractor_shareable_p(val)) {
rb_raise(rb_eRactorIsolationError,
"can not copy a class/module created by another Ractor because "
"variable %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
rb_ractor_isolation_violation(
"can not copy a class/module created by another Ractor because "
"variable %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
}
return ST_CONTINUE;
}
Expand Down Expand Up @@ -2447,8 +2446,7 @@ rb_class_attached_object(VALUE klass)
if (rb_objspace_foreign_object_p(obj) && !RB_OBJ_SHAREABLE_P(obj)) {
/* No klass in the message: naming a singleton class inspects the very object we
* must not touch from here. */
rb_raise(rb_eRactorIsolationError,
"can not get an unshareable attached object from another Ractor");
rb_ractor_isolation_violation("can not get an unshareable attached object from another Ractor");
}

return obj;
Expand Down
2 changes: 2 additions & 0 deletions cont.c
Original file line number Diff line number Diff line change
Expand Up @@ -2317,6 +2317,8 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking)
fiber->blocking = blocking;
fiber->killed = 0;
cont_init(&fiber->cont, th);
// A warning hook in the creator must not silence this new fiber.
fiber->cont.saved_ec.ractor_isolation_warning = false;

fiber->cont.saved_ec.fiber_ptr = fiber;
fiber->cont.saved_ec.serial = next_ec_serial(th->ractor);
Expand Down
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_ISOLATION
* (downgraded from Ractor::IsolationError exceptions to warnings).
*/

static VALUE
Expand Down Expand Up @@ -3926,6 +3931,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 @@ -3939,13 +3945,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
24 changes: 22 additions & 2 deletions gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2176,8 +2176,13 @@ undefine_final(VALUE os, VALUE obj)
VALUE
rb_undefine_finalizer(VALUE obj)
{
rb_check_frozen(obj);
if (rb_objspace_foreign_object_p(obj)) {
rb_ractor_isolation_violation(
"can not undefine a finalizer of an object of another Ractor");
return obj;
}

rb_check_frozen(obj);
rb_gc_impl_undefine_finalizer(rb_gc_get_objspace(), obj);

return obj;
Expand Down Expand Up @@ -2298,7 +2303,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 Expand Up @@ -4393,6 +4404,15 @@ rb_gc_vm_ractor_count(void)
return GET_VM()->ractor.cnt;
}

extern int ruby_ractor_isolation_enabled;

/* Check mode passes objects between Ractors by reference; only a global cycle sees those edges. */
bool
rb_gc_vm_global_gc_only_p(void)
{
return ruby_ractor_isolation_enabled != 0;
}

/* Called by a global cycle from inside the barrier. */
void
rb_gc_vm_refresh_zombie_pages(void)
Expand Down
16 changes: 15 additions & 1 deletion gc/default/default.c
Original file line number Diff line number Diff line change
Expand Up @@ -7005,11 +7005,13 @@ check_children_i(const VALUE child, void *ptr)
* invisible to both local GCs. The exception is a box's top_self, which every
* thread's th->top_self points at and which is VM-permanent. Skipped during a
* global GC: it clears every shref bit, so the shref exemption would not fire,
* and its unified exact stop-the-world mark makes the invariant itself moot. */
* and its unified exact stop-the-world mark makes the invariant itself moot.
* Isolation checking also allows these edges and uses only global GC. */
if (!data->parent_shareable &&
child != rb_gc_vm_top_self() &&
!MARKED_IN_BITMAP(GET_HEAP_SHAREABLE_BITS(child), child) &&
!MARKED_IN_BITMAP(GET_HEAP_SHREF_BITS(child), child) &&
!rb_gc_vm_global_gc_only_p() &&
!rb_gc_impl_during_global_gc_p(data->objspace) &&
!global_objspace->during_absorb) {
fprintf(stderr, "check_children_i: containment violation: "
Expand Down Expand Up @@ -7079,6 +7081,8 @@ root_scope_check_i(const char *category, VALUE obj, void *ptr)
return;
}

/* Isolation checking permits foreign unshareable roots and uses only global GC. */
if (rb_gc_vm_global_gc_only_p()) return;
if (GET_HEAP_OBJSPACE(obj) == data->objspace) return;
if (MARKED_IN_BITMAP(GET_HEAP_SHAREABLE_BITS(obj), obj)) return;
if (MARKED_IN_BITMAP(GET_HEAP_SHREF_BITS(obj), obj)) return;
Expand Down Expand Up @@ -8668,6 +8672,12 @@ rb_gc_impl_objspace_retire_gc(void *objspace_ptr)
{
rb_objspace_t *objspace = objspace_ptr;

/* Other Ractors may hold this heap's objects by reference; the next global cycle sweeps it. */
if (rb_gc_vm_global_gc_only_p()) {
gc_rest(objspace);
return;
}

/* The dying thread's stack is already torn down here, so the root scan must skip
* its machine context (rb_gc_mark_roots). */
objspace->flags.during_postmortem = 1;
Expand Down Expand Up @@ -8821,6 +8831,7 @@ static bool
gc_need_global_p(rb_objspace_t *objspace)
{
if (rb_gc_single_objspace_p()) return false;
if (rb_gc_vm_global_gc_only_p()) return true;
/* A Ractor's death must not stop the world, so the retire GC stays local. */
if (objspace->flags.during_postmortem) return false;
if (objspace->shareable_objects > objspace->shareable_objects_limit) return true;
Expand Down Expand Up @@ -10245,6 +10256,9 @@ rb_gc_impl_start(void *objspace_ptr, bool full_mark, bool immediate_mark, bool i
global = false;
}

/* Isolation checking never collects one objspace alone (see gc_need_global_p). */
if (rb_gc_vm_global_gc_only_p()) global = true;

if (global && !rb_gc_single_objspace_p()) {
/* A mid-cycle driver is settled by gc_start_global itself: it aborts the partial
* mark and finishes the lazy sweep, so the dead slots are T_NONE before the
Expand Down
1 change: 1 addition & 0 deletions gc/gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ MODULAR_GC_FN bool rb_gc_registered_addr_owned_by_registrant_p(VALUE *addr, void
MODULAR_GC_FN bool rb_gc_vm_zombie_objspace_p(void *objspace);
MODULAR_GC_FN size_t rb_gc_vm_zombie_total_pages(void);
MODULAR_GC_FN unsigned int rb_gc_vm_ractor_count(void);
MODULAR_GC_FN bool rb_gc_vm_global_gc_only_p(void);
MODULAR_GC_FN void rb_gc_vm_refresh_zombie_pages(void);
/* No MODULAR_GC_FN: the VM side (ractor.c) calls this too, so it needs external
* linkage even in a non-modular build (see internal/gc.h). */
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_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
6 changes: 5 additions & 1 deletion process.c
Original file line number Diff line number Diff line change
Expand Up @@ -4062,7 +4062,11 @@ 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");

// only reached in check mode; fork would drop every other Ractor's thread
errno = EPERM;
return -1;
}

struct rb_process_status child = {.status = 0};
Expand Down
Loading
Loading