Skip to content

Commit 946b208

Browse files
committed
Add Ractor.check_isolation
Run the block in a real non-main Ractor while preserving its closure and argument identities. Downgrade isolation violations to categorized warnings so applications can sweep worker-Ractor compatibility without stopping at the first failure. Support an exclusive scheduler mode for race-free checks and cover the isolation gates, fast paths, messaging, and thread inheritance.
1 parent 76ce036 commit 946b208

18 files changed

Lines changed: 817 additions & 72 deletions

error.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ static ID id_deprecated;
8989
static ID id_experimental;
9090
static ID id_performance;
9191
static ID id_strict_unused_block;
92+
static ID id_ractor_isolation;
9293
static VALUE sym_category;
9394
static VALUE sym_highlight;
9495
static struct {
@@ -224,6 +225,10 @@ rb_warning_category_enabled_p(rb_warning_category_t category)
224225
* +:performance+ ::
225226
* performance hints
226227
* * Shape variation limit
228+
*
229+
* +:ractor_isolation+ ::
230+
* Ractor isolation violations reported by Ractor.check_isolation
231+
* (downgraded from Ractor::IsolationError exceptions to warnings).
227232
*/
228233

229234
static VALUE
@@ -3884,6 +3889,7 @@ Init_Exception(void)
38843889
id_experimental = rb_intern_const("experimental");
38853890
id_performance = rb_intern_const("performance");
38863891
id_strict_unused_block = rb_intern_const("strict_unused_block");
3892+
id_ractor_isolation = rb_intern_const("ractor_isolation");
38873893
id_top = rb_intern_const("top");
38883894
id_bottom = rb_intern_const("bottom");
38893895
id_iseq = rb_make_internal_id();
@@ -3897,13 +3903,15 @@ Init_Exception(void)
38973903
st_add_direct(warning_categories.id2enum, id_experimental, RB_WARN_CATEGORY_EXPERIMENTAL);
38983904
st_add_direct(warning_categories.id2enum, id_performance, RB_WARN_CATEGORY_PERFORMANCE);
38993905
st_add_direct(warning_categories.id2enum, id_strict_unused_block, RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK);
3906+
st_add_direct(warning_categories.id2enum, id_ractor_isolation, RB_WARN_CATEGORY_RACTOR_ISOLATION);
39003907

39013908
warning_categories.enum2id = rb_init_identtable();
39023909
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_NONE, 0);
39033910
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_DEPRECATED, id_deprecated);
39043911
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_EXPERIMENTAL, id_experimental);
39053912
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_PERFORMANCE, id_performance);
39063913
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK, id_strict_unused_block);
3914+
st_add_direct(warning_categories.enum2id, RB_WARN_CATEGORY_RACTOR_ISOLATION, id_ractor_isolation);
39073915
}
39083916

39093917
void

gc.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,12 @@ rb_undefine_finalizer(VALUE obj)
20972097
{
20982098
rb_check_frozen(obj);
20992099

2100+
if (rb_gc_obj_foreign_p(obj)) {
2101+
rb_ractor_isolation_violation(
2102+
"can not undefine a finalizer of an object of another Ractor");
2103+
return obj;
2104+
}
2105+
21002106
rb_gc_impl_undefine_finalizer(rb_gc_get_objspace(), obj);
21012107

21022108
return obj;
@@ -2217,7 +2223,13 @@ rb_define_finalizer(VALUE obj, VALUE block)
22172223
should_be_finalizable(obj);
22182224
should_be_callable(block);
22192225

2220-
block = rb_gc_impl_define_finalizer(rb_gc_get_objspace(), obj, block);
2226+
if (rb_gc_obj_foreign_p(obj)) {
2227+
rb_ractor_isolation_violation(
2228+
"can not define a finalizer for an object of another Ractor");
2229+
}
2230+
else {
2231+
block = rb_gc_impl_define_finalizer(rb_gc_get_objspace(), obj, block);
2232+
}
22212233

22222234
block = rb_ary_new3(2, INT2FIX(0), block);
22232235
OBJ_FREEZE(block);

include/ruby/internal/error.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,21 @@ typedef enum {
5656
/** Warning is for checking unused block strictly */
5757
RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK,
5858

59+
/** Warning is for Ractor isolation violations reported by Ractor.check_isolation. */
60+
RB_WARN_CATEGORY_RACTOR_ISOLATION,
61+
5962
RB_WARN_CATEGORY_DEFAULT_BITS = (
6063
(1U << RB_WARN_CATEGORY_DEPRECATED) |
6164
(1U << RB_WARN_CATEGORY_EXPERIMENTAL) |
65+
(1U << RB_WARN_CATEGORY_RACTOR_ISOLATION) |
6266
0),
6367

6468
RB_WARN_CATEGORY_ALL_BITS = (
6569
(1U << RB_WARN_CATEGORY_DEPRECATED) |
6670
(1U << RB_WARN_CATEGORY_EXPERIMENTAL) |
6771
(1U << RB_WARN_CATEGORY_PERFORMANCE) |
6872
(1U << RB_WARN_CATEGORY_STRICT_UNUSED_BLOCK) |
73+
(1U << RB_WARN_CATEGORY_RACTOR_ISOLATION) |
6974
0)
7075
} rb_warning_category_t;
7176

process.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4109,7 +4109,7 @@ rb_pid_t
41094109
rb_fork_ruby(int *status)
41104110
{
41114111
if (UNLIKELY(!rb_ractor_main_p())) {
4112-
rb_raise(rb_eRactorIsolationError, "can not fork from non-main Ractors");
4112+
rb_ractor_isolation_violation("can not fork from non-main Ractors");
41134113
}
41144114

41154115
struct rb_process_status child = {.status = 0};

ractor.c

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "ruby/ractor.h"
66
#include "ruby/re.h"
77
#include "ruby/thread_native.h"
8+
#include "ruby_atomic.h"
89
#include "vm_core.h"
910
#include "vm_sync.h"
1011
#include "ractor_core.h"
@@ -840,11 +841,12 @@ rb_ractor_main_setup(rb_vm_t *vm, rb_ractor_t *r, rb_thread_t *th)
840841
}
841842

842843
static VALUE
843-
ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block)
844+
ractor_create0(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block, bool isolation_check)
844845
{
845846
VALUE rv = ractor_alloc(self);
846847
rb_ractor_t *r = RACTOR_PTR(rv);
847848
ractor_init(r, name, loc);
849+
r->isolation_check = isolation_check;
848850

849851
r->pub.id = ractor_next_id();
850852
RUBY_DEBUG_LOG("r:%u", r->pub.id);
@@ -863,6 +865,12 @@ ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VAL
863865
return rv;
864866
}
865867

868+
static VALUE
869+
ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block)
870+
{
871+
return ractor_create0(ec, self, loc, name, args, block, false);
872+
}
873+
866874
#if 0
867875
static VALUE
868876
ractor_create_func(VALUE klass, VALUE loc, VALUE name, VALUE args, rb_block_call_func_t func)
@@ -1850,6 +1858,12 @@ make_shareable_check_shareable(VALUE obj)
18501858
}
18511859
else if (!allow_frozen_shareable_p(obj)) {
18521860
if (!RB_TYPE_P(obj, T_DATA)) {
1861+
if (rb_ractor_isolation_check_p()) {
1862+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
1863+
"can not make shareable object of class %+"PRIsVALUE,
1864+
rb_class_of(obj));
1865+
return traverse_stop;
1866+
}
18531867
rb_raise(rb_eRactorError,
18541868
"can not make shareable object for %+"PRIsVALUE, obj);
18551869
}
@@ -1859,14 +1873,26 @@ make_shareable_check_shareable(VALUE obj)
18591873
RB_OBJ_SET_SHAREABLE(obj);
18601874
return traverse_skip;
18611875
}
1876+
else if (rb_ractor_isolation_check_p()) {
1877+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
1878+
"can not make shareable object of class %+"PRIsVALUE
1879+
" because it refers unshareable objects", rb_class_of(obj));
1880+
return traverse_stop;
1881+
}
18621882
else {
18631883
rb_raise(rb_eRactorError,
18641884
"can not make shareable object for %+"PRIsVALUE" because it refers unshareable objects", obj);
18651885
}
18661886
}
18671887
else if (rb_obj_is_proc(obj)) {
18681888
rb_proc_ractor_make_shareable(obj, Qundef);
1869-
return traverse_cont;
1889+
return rb_ractor_shareable_p(obj) ? traverse_cont : traverse_stop;
1890+
}
1891+
else if (rb_ractor_isolation_check_p()) {
1892+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
1893+
"can not make shareable object of class %+"PRIsVALUE,
1894+
rb_class_of(obj));
1895+
return traverse_stop;
18701896
}
18711897
else {
18721898
rb_raise(rb_eRactorError, "can not make shareable object for %+"PRIsVALUE, obj);
@@ -1930,9 +1956,10 @@ VALUE
19301956
rb_ractor_ensure_shareable(VALUE obj, VALUE name)
19311957
{
19321958
if (!rb_ractor_shareable_p(obj)) {
1933-
VALUE message = rb_sprintf("cannot assign unshareable object to %"PRIsVALUE,
1934-
name);
1935-
rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, message));
1959+
rb_ractor_isolation_violation("cannot assign unshareable object to %"PRIsVALUE, name);
1960+
// In check_isolation mode the violation only warned: return obj as-is
1961+
// so the caller can keep going. The caller's invariant ("this is now
1962+
// shareable") will be wrong, which is exactly the bug we want surfaced.
19361963
}
19371964
return obj;
19381965
}
@@ -1941,7 +1968,7 @@ void
19411968
rb_ractor_ensure_main_ractor(const char *msg)
19421969
{
19431970
if (!rb_ractor_main_p()) {
1944-
rb_raise(rb_eRactorIsolationError, "%s", msg);
1971+
rb_ractor_isolation_violation("%s", msg);
19451972
}
19461973
}
19471974

@@ -3783,13 +3810,11 @@ ractor_local_value_store_if_absent(rb_execution_context_t *ec, VALUE self, VALUE
37833810
static VALUE
37843811
ractor_shareable_proc(rb_execution_context_t *ec, VALUE replace_self, bool is_lambda)
37853812
{
3786-
if (!rb_ractor_shareable_p(replace_self)) {
3787-
rb_raise(rb_eRactorIsolationError, "self should be shareable: %" PRIsVALUE, replace_self);
3788-
}
3789-
else {
3790-
VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc();
3791-
return rb_proc_ractor_make_shareable(rb_proc_dup(proc), replace_self);
3813+
if (!rb_ractor_shareable_p(replace_self) && !rb_ractor_isolation_check_p()) {
3814+
rb_ractor_isolation_violation("self should be shareable: %" PRIsVALUE, replace_self);
37923815
}
3816+
VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc();
3817+
return rb_proc_ractor_make_shareable(rb_proc_dup(proc), replace_self);
37933818
}
37943819

37953820
// Ractor#require
@@ -4001,4 +4026,76 @@ rb_ractor_autoload_load(VALUE module, ID name)
40014026
}
40024027
}
40034028

4029+
// =============================================================================
4030+
// Ractor.check_isolation { ... }
4031+
//
4032+
// A development/debugging mode: the block runs in a genuine non-main Ractor,
4033+
// without isolating its Proc or copying its arguments. Violations are
4034+
// downgraded from Ractor::IsolationError to :ractor_isolation category warnings
4035+
// so the program can keep running and report more than the first violation.
4036+
//
4037+
// As a side effect (matches Ractor semantics), the VM is switched into
4038+
// multi-ractor mode the first time check_isolation is enabled. Multi-ractor
4039+
// mode cannot be turned off again, so the VM keeps paying that overhead for
4040+
// the rest of the process lifetime.
4041+
// =============================================================================
4042+
4043+
bool
4044+
rb_ractor_isolation_check_p(void)
4045+
{
4046+
rb_execution_context_t *ec = rb_current_ec_noinline();
4047+
if (!ec) return false;
4048+
rb_ractor_t *r = rb_ec_ractor_ptr(ec);
4049+
return r && r->isolation_check;
4050+
}
4051+
4052+
void
4053+
rb_ractor_isolation_violation_str(VALUE message)
4054+
{
4055+
if (rb_ractor_isolation_check_p()) {
4056+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION, "%s", StringValueCStr(message));
4057+
return;
4058+
}
4059+
4060+
rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, message));
4061+
}
4062+
4063+
void
4064+
rb_ractor_isolation_violation(const char *fmt, ...)
4065+
{
4066+
va_list args;
4067+
va_start(args, fmt);
4068+
VALUE message = rb_vsprintf(fmt, args);
4069+
va_end(args);
4070+
4071+
rb_ractor_isolation_violation_str(message);
4072+
}
4073+
4074+
/* Set during native-thread scheduler initialization; see thread_sched.c. */
4075+
extern int ruby_ractor_exclusive_enabled;
4076+
4077+
static rb_atomic_t ractor_check_isolation_advisory_emitted;
4078+
4079+
/* Return true to exactly one caller when nonexclusive mode needs its advisory.
4080+
* This state cannot live on Ractor itself: setting a class/module ivar from an
4081+
* ordinary non-main Ractor is itself an isolation violation. */
4082+
static VALUE
4083+
ractor_check_isolation_warn_p(rb_execution_context_t *ec, VALUE self)
4084+
{
4085+
if (ruby_ractor_exclusive_enabled) return Qfalse;
4086+
return RBOOL(ATOMIC_EXCHANGE(ractor_check_isolation_advisory_emitted, 1) == 0);
4087+
}
4088+
4089+
static VALUE
4090+
ractor_check_isolation_p(rb_execution_context_t *ec, VALUE self)
4091+
{
4092+
return RBOOL(rb_ractor_isolation_check_p());
4093+
}
4094+
4095+
static VALUE
4096+
ractor_check_isolation_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block)
4097+
{
4098+
return ractor_create0(ec, self, loc, name, args, block, true);
4099+
}
4100+
40044101
#include "ractor.rbinc"

ractor.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,50 @@ def self.main?
538538
}
539539
end
540540

541+
# call-seq:
542+
# Ractor.check_isolation(*args, name: nil) {|*args| ... } -> result of block
543+
#
544+
# Runs the block in a genuine non-main \Ractor while downgrading isolation
545+
# violations to +:ractor_isolation+ category warnings. Unlike +Ractor.new+,
546+
# the block is not isolated and its arguments are passed by reference, so it
547+
# can close over and inspect existing non-shareable application state.
548+
#
549+
# The block therefore observes production worker-Ractor behavior:
550+
# +Ractor.main?+ is false and +Ractor.current+ is the newly created \Ractor.
551+
# Its return value is delivered through +Ractor#value+, and exceptions are
552+
# re-raised there as for an ordinary \Ractor.
553+
#
554+
# On builds with M:N scheduling, booting with +RUBY_RACTOR_EXCLUSIVE=1+
555+
# limits the scheduler to one shared native thread. This prevents simultaneous
556+
# Ruby execution on shared native threads, but does not make the block atomic:
557+
# blocking operations can hand the run slot to another \Ractor, and dedicated
558+
# native threads are not covered. Without that mode, this method emits a
559+
# one-time advisory because other \Ractors may run in parallel.
560+
#
561+
# Calling this method switches the VM into multi-Ractor mode permanently.
562+
# Suppress isolation warnings with +Warning[:ractor_isolation] = false+ or
563+
# +-W:no-ractor_isolation+.
564+
def self.check_isolation(*args, name: nil, &block)
565+
b = block # TODO: builtin bug
566+
raise ArgumentError, "must be called with a block" unless block
567+
568+
if Primitive.ractor_check_isolation_warn_p
569+
Kernel.warn("Ractor.check_isolation: other Ractors can run in parallel " \
570+
"with the isolation-check Ractor. On builds with M:N scheduling, " \
571+
"RUBY_RACTOR_EXCLUSIVE=1 prevents simultaneous Ruby execution on " \
572+
"shared native threads.", uplevel: 1)
573+
end
574+
575+
loc = caller_locations(1, 1).first
576+
loc = "#{loc.path}:#{loc.lineno}"
577+
Primitive.ractor_check_isolation_create(loc, name, args, b).value
578+
end
579+
580+
# Returns true when the current \Ractor is running an isolation check.
581+
def self.check_isolation?
582+
Primitive.ractor_check_isolation_p
583+
end
584+
541585
# internal method
542586
def self._require feature # :nodoc:
543587
if main?

ractor_core.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ struct rb_ractor_struct {
144144

145145
bool malloc_gc_disabled;
146146
bool main_ractor;
147+
bool isolation_check;
147148
void *newobj_cache;
148149

149150
/* This Ractor's objspace. The main Ractor receives the boot objspace from
@@ -235,6 +236,20 @@ VALUE rb_ractor_autoload_load(VALUE space, ID id);
235236
VALUE rb_ractor_ensure_shareable(VALUE obj, VALUE name);
236237
st_table *rb_ractor_targeted_hooks(rb_ractor_t *cr);
237238

239+
/* True if the current Ractor was created by Ractor.check_isolation. */
240+
bool rb_ractor_isolation_check_p(void);
241+
242+
/* Report a Ractor isolation violation:
243+
* - if Ractor.check_isolation is active on the current Ractor, emit a
244+
* :ractor_isolation category warning and return;
245+
* - otherwise, raise Ractor::IsolationError (does not return).
246+
*
247+
* Use the printf-style overload for ad-hoc messages and the _str overload
248+
* when the message is already constructed (e.g. via several rb_str_catf
249+
* calls). */
250+
PRINTF_ARGS(void rb_ractor_isolation_violation(const char *fmt, ...), 1, 2);
251+
void rb_ractor_isolation_violation_str(VALUE message);
252+
238253
RUBY_SYMBOL_EXPORT_BEGIN
239254
void rb_ractor_finish_marking(bool full_mark);
240255

ractor_sync.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,20 @@ ractor_prepare_payload(rb_execution_context_t *ec, VALUE obj, enum ractor_basket
10681068
*ptype = basket_type_ref;
10691069
return obj;
10701070
}
1071+
else if (rb_ractor_isolation_check_p()) {
1072+
// Under Ractor.check_isolation, don't copy non-shareable messages.
1073+
// Copying can fail outright (e.g. Procs -> "can not copy Proc
1074+
// object"), which would abort a real-Ractor sweep at the first
1075+
// Ractor::Dispatch call. Exclusive mode (RUBY_RACTOR_EXCLUSIVE)
1076+
// guarantees no other Ractor runs concurrently, so passing the
1077+
// original object by reference is safe; warn and continue.
1078+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
1079+
"can not copy an unshareable %"PRIsVALUE" across Ractors; "
1080+
"passing by reference under Ractor.check_isolation",
1081+
rb_class_of(obj));
1082+
*ptype = basket_type_ref;
1083+
return obj;
1084+
}
10711085
else {
10721086
/* Snapshot the object on the sender side without calling the user-visible
10731087
* #clone. Both forms are off-heap, so an in-flight payload is never a GC

0 commit comments

Comments
 (0)