Skip to content

Commit 9dacbfd

Browse files
committed
And Ractor.warn_frozen_error. This is to pair with Ractor.check_isolation. In this mode objects are not frozen by Ractor.make_shareable. But a warning will be emitted if the object is mutated.
1 parent 75cd024 commit 9dacbfd

9 files changed

Lines changed: 314 additions & 6 deletions

File tree

error.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ rb_warning_category_enabled_p(rb_warning_category_t category)
227227
*
228228
* +:ractor_isolation+ ::
229229
* Ractor isolation violations reported by Ractor.check_isolation
230-
* (downgraded from Ractor::IsolationError exceptions to warnings).
230+
* (downgraded from Ractor::IsolationError exceptions to warnings), and
231+
* FrozenError warnings reported for objects passed to Ractor.make_shareable
232+
* while Ractor.warn_frozen_error is enabled.
231233
*/
232234

233235
static VALUE

include/ruby/internal/intern/error.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ RBIMPL_ATTR_NORETURN()
201201
*/
202202
void rb_error_frozen_object(VALUE what);
203203

204+
/**
205+
* True once Ractor.warn_frozen_error has marked any objects whose would-be
206+
* FrozenError should be reported as a warning instead.
207+
*
208+
* @internal
209+
*/
210+
RUBY_EXTERN bool ruby_ractor_warn_frozen_error_objects_enabled;
211+
212+
/**
213+
* Emits a warning and returns true if +obj+ was recorded by Ractor.make_shareable
214+
* while Ractor.warn_frozen_error was enabled.
215+
*
216+
* @internal
217+
*/
218+
bool rb_ractor_warn_frozen_error_warn(VALUE obj);
219+
204220
/**
205221
* Queries if the passed object is frozen.
206222
*
@@ -253,8 +269,16 @@ static inline void
253269
rb_check_frozen_inline(VALUE obj)
254270
{
255271
if (RB_UNLIKELY(RB_OBJ_FROZEN(obj))) {
272+
if (RB_UNLIKELY(ruby_ractor_warn_frozen_error_objects_enabled) &&
273+
rb_ractor_warn_frozen_error_warn(obj)) {
274+
return;
275+
}
256276
rb_error_frozen_object(obj);
257277
}
278+
else if (RB_UNLIKELY(ruby_ractor_warn_frozen_error_objects_enabled) &&
279+
!(RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, RUBY_FL_USER2 | RUBY_FL_USER3))) {
280+
rb_ractor_warn_frozen_error_warn(obj);
281+
}
258282

259283
/* ref: internal CHILLED_STRING_P()
260284
This is an implementation detail subject to change. */

ractor.c

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,67 @@ rb_obj_set_shareable(VALUE obj)
12291229
return obj;
12301230
}
12311231

1232+
/* Global opt-in that makes Ractor.make_shareable record objects as
1233+
* "warning-shareable" instead of freezing them. Those objects are kept in an
1234+
* identity hash so rb_check_frozen() and selected fast paths can warn if they
1235+
* are mutated. */
1236+
bool ruby_ractor_warn_frozen_error = false;
1237+
bool ruby_ractor_warn_frozen_error_objects_enabled = false;
1238+
static VALUE ractor_warn_frozen_error_objects = Qnil;
1239+
1240+
static VALUE
1241+
ractor_warn_frozen_error_objects_hash(void)
1242+
{
1243+
if (NIL_P(ractor_warn_frozen_error_objects)) {
1244+
ractor_warn_frozen_error_objects = rb_ident_hash_new();
1245+
rb_obj_hide(ractor_warn_frozen_error_objects);
1246+
rb_gc_register_mark_object(ractor_warn_frozen_error_objects);
1247+
}
1248+
return ractor_warn_frozen_error_objects;
1249+
}
1250+
1251+
bool
1252+
rb_ractor_warn_frozen_error_marked_p(VALUE obj)
1253+
{
1254+
if (RB_SPECIAL_CONST_P(obj) || !ruby_ractor_warn_frozen_error_objects_enabled) {
1255+
return false;
1256+
}
1257+
1258+
if (NIL_P(ractor_warn_frozen_error_objects)) {
1259+
return false;
1260+
}
1261+
1262+
return RTEST(rb_hash_lookup2(ractor_warn_frozen_error_objects, obj, Qfalse));
1263+
}
1264+
1265+
bool
1266+
rb_ractor_warn_frozen_error_warn(VALUE obj)
1267+
{
1268+
if (!rb_ractor_warn_frozen_error_marked_p(obj)) {
1269+
return false;
1270+
}
1271+
1272+
rb_category_warn(RB_WARN_CATEGORY_RACTOR_ISOLATION,
1273+
"would raise FrozenError: can't modify object passed to Ractor.make_shareable with Ractor.warn_frozen_error=true: %"PRIsVALUE,
1274+
rb_obj_class(obj));
1275+
return true;
1276+
}
1277+
1278+
static void
1279+
ractor_warn_frozen_error_mark(VALUE obj)
1280+
{
1281+
if (RB_SPECIAL_CONST_P(obj)) {
1282+
return;
1283+
}
1284+
1285+
if (RB_TYPE_P(obj, T_STRING)) {
1286+
rb_str_make_independent(obj);
1287+
}
1288+
1289+
rb_hash_aset(ractor_warn_frozen_error_objects_hash(), obj, Qtrue);
1290+
ruby_ractor_warn_frozen_error_objects_enabled = true;
1291+
}
1292+
12321293
/// traverse function
12331294

12341295
// 2: stop search
@@ -1639,6 +1700,64 @@ make_shareable_check_shareable(VALUE obj, struct obj_traverse_data *data)
16391700
return make_shareable_check_shareable_freeze(obj, traverse_cont, data);
16401701
}
16411702

1703+
static enum obj_traverse_iterator_result
1704+
make_shareable_warn_check_shareable(VALUE obj, struct obj_traverse_data *data)
1705+
{
1706+
VM_ASSERT(!SPECIAL_CONST_P(obj));
1707+
1708+
if (rb_ractor_shareable_p(obj)) {
1709+
return traverse_skip;
1710+
}
1711+
else if (!allow_frozen_shareable_p(obj)) {
1712+
VM_ASSERT(RB_TYPE_P(obj, T_DATA));
1713+
const rb_data_type_t *type = RTYPEDDATA_TYPE(obj);
1714+
1715+
if (type->flags & RUBY_TYPED_FROZEN_SHAREABLE_NO_REC) {
1716+
if (obj_refer_only_shareables_p(obj)) {
1717+
ractor_warn_frozen_error_mark(obj);
1718+
return traverse_skip;
1719+
}
1720+
else {
1721+
rb_raise(rb_eRactorError,
1722+
"can not make shareable object for %+"PRIsVALUE" because it refers unshareable objects", obj);
1723+
}
1724+
}
1725+
else if (rb_obj_is_proc(obj)) {
1726+
if (!rb_proc_ractor_make_shareable_continue(obj, Qundef, data->chain)) {
1727+
rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(obj);
1728+
if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet");
1729+
1730+
if (data->exception) {
1731+
*data->exception = rb_exc_new3(rb_eRactorIsolationError, rb_sprintf("Proc's self is not shareable: %" PRIsVALUE, obj));
1732+
}
1733+
return traverse_stop;
1734+
}
1735+
return traverse_cont;
1736+
}
1737+
else {
1738+
return traverse_stop;
1739+
}
1740+
}
1741+
1742+
switch (TYPE(obj)) {
1743+
case T_IMEMO:
1744+
return traverse_skip;
1745+
case T_OBJECT:
1746+
break;
1747+
default:
1748+
break;
1749+
}
1750+
1751+
return traverse_cont;
1752+
}
1753+
1754+
static enum obj_traverse_iterator_result
1755+
mark_warn_shareable(VALUE obj)
1756+
{
1757+
ractor_warn_frozen_error_mark(obj);
1758+
return traverse_cont;
1759+
}
1760+
16421761
static enum obj_traverse_iterator_result
16431762
mark_shareable(VALUE obj)
16441763
{
@@ -1655,7 +1774,12 @@ rb_ractor_make_shareable(VALUE obj)
16551774
{
16561775
VALUE chain = Qnil;
16571776
VALUE exception = Qfalse;
1658-
if (rb_obj_traverse(obj, make_shareable_check_shareable, null_leave, mark_shareable, &chain, &exception)) {
1777+
rb_obj_traverse_enter_func enter_func = ruby_ractor_warn_frozen_error ?
1778+
make_shareable_warn_check_shareable : make_shareable_check_shareable;
1779+
rb_obj_traverse_final_func final_func = ruby_ractor_warn_frozen_error ?
1780+
mark_warn_shareable : mark_shareable;
1781+
1782+
if (rb_obj_traverse(obj, enter_func, null_leave, final_func, &chain, &exception)) {
16591783
if (!exception) {
16601784
exception = rb_exc_new3(rb_eRactorError, rb_sprintf("can not make shareable object for %+"PRIsVALUE, obj));
16611785
}
@@ -2235,7 +2359,11 @@ ractor_obj_clone(VALUE obj)
22352359
static enum obj_traverse_iterator_result
22362360
copy_enter(VALUE obj, struct obj_traverse_replace_data *data)
22372361
{
2238-
if (rb_ractor_shareable_p(obj)) {
2362+
if (rb_ractor_warn_frozen_error_marked_p(obj)) {
2363+
data->replacement = ractor_obj_clone(obj);
2364+
return traverse_cont;
2365+
}
2366+
else if (rb_ractor_shareable_p(obj)) {
22392367
data->replacement = obj;
22402368
return traverse_skip;
22412369
}
@@ -2914,4 +3042,17 @@ ractor_check_isolation_p(rb_execution_context_t *ec, VALUE self)
29143042
return rb_thread_ractor_isolation_check_p() ? Qtrue : Qfalse;
29153043
}
29163044

3045+
static VALUE
3046+
ractor_warn_frozen_error(rb_execution_context_t *ec, VALUE self)
3047+
{
3048+
return ruby_ractor_warn_frozen_error ? Qtrue : Qfalse;
3049+
}
3050+
3051+
static VALUE
3052+
ractor_warn_frozen_error_set(rb_execution_context_t *ec, VALUE self, VALUE enabled)
3053+
{
3054+
ruby_ractor_warn_frozen_error = RTEST(enabled);
3055+
return enabled;
3056+
}
3057+
29173058
#include "ractor.rbinc"

ractor.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ def self.shareable? obj
433433
# If +copy+ keyword is +true+, it will copy objects before freezing them, and will not
434434
# modify +obj+ or its internal objects.
435435
#
436+
# If +Ractor.warn_frozen_error+ is enabled, this method records the reachable
437+
# object graph for mutation warnings instead of freezing it or making it
438+
# actually shareable. This mode is intended only for development sweeps.
439+
#
436440
# Note that the specification and implementation of this method are not
437441
# mature and may be changed in the future.
438442
#
@@ -565,6 +569,37 @@ def self.check_isolation?
565569
Primitive.ractor_check_isolation_p
566570
end
567571

572+
# call-seq:
573+
# Ractor.warn_frozen_error -> true or false
574+
# Ractor.warn_frozen_error? -> true or false
575+
# Ractor.warn_frozen_error = flag -> flag
576+
#
577+
# When enabled, +Ractor.make_shareable+ records the reachable object graph as
578+
# warning-shareable instead of freezing it or making it actually shareable
579+
# between real ractors. Later attempts to mutate those objects emit a
580+
# +:ractor_isolation+ category warning that identifies the operation that
581+
# would have raised +FrozenError+, then allow the mutation to continue. This
582+
# is intended for development sweeps alongside +Ractor.check_isolation+, so
583+
# code can discover objects that would be frozen by production
584+
# +make_shareable+ without changing runtime behaviour as much.
585+
#
586+
# The setting affects future +make_shareable+ calls. Objects already marked
587+
# warning-shareable keep warning on mutation even if the setting is disabled.
588+
# Suppress the warnings with +Warning[:ractor_isolation] = false+ or
589+
# +-W:no-ractor_isolation+.
590+
def self.warn_frozen_error
591+
Primitive.ractor_warn_frozen_error
592+
end
593+
594+
def self.warn_frozen_error=(flag)
595+
Primitive.ractor_warn_frozen_error_set(flag)
596+
end
597+
598+
# Returns true when Ractor.warn_frozen_error is enabled.
599+
def self.warn_frozen_error?
600+
Primitive.ractor_warn_frozen_error
601+
end
602+
568603
# internal method
569604
def self._require feature # :nodoc:
570605
if main?

ractor_core.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ RUBY_EXTERN bool ruby_ractor_isolation_check_enabled;
172172
* rb_ractor_isolation_check_active(). */
173173
bool rb_thread_ractor_isolation_check_p(void);
174174

175+
/* True if obj was made warning-shareable by Ractor.make_shareable while
176+
* Ractor.warn_frozen_error was enabled. */
177+
bool rb_ractor_warn_frozen_error_marked_p(VALUE obj);
178+
175179
/* Report a Ractor isolation violation:
176180
* - if Ractor.check_isolation is active on the current thread, emit a
177181
* :ractor_isolation category warning and return;

ractor_sync.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,11 @@ ractor_prepare_payload(rb_execution_context_t *ec, VALUE obj, enum ractor_basket
777777
case basket_type_move:
778778
return ractor_move(obj);
779779
default:
780-
if (rb_ractor_shareable_p(obj)) {
780+
if (rb_ractor_warn_frozen_error_marked_p(obj)) {
781+
*ptype = basket_type_copy;
782+
return ractor_copy(obj);
783+
}
784+
else if (rb_ractor_shareable_p(obj)) {
781785
*ptype = basket_type_ref;
782786
return obj;
783787
}

string.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2673,6 +2673,9 @@ str_modifiable(VALUE str)
26732673
rb_check_lockedtmp(str);
26742674
rb_check_frozen(str);
26752675
}
2676+
else if (RB_UNLIKELY(ruby_ractor_warn_frozen_error_objects_enabled)) {
2677+
rb_check_frozen(str);
2678+
}
26762679
}
26772680

26782681
static inline int
@@ -2694,7 +2697,7 @@ str_independent(VALUE str)
26942697
{
26952698
RUBY_ASSERT(ruby_thread_has_gvl_p());
26962699

2697-
if (RB_UNLIKELY(FL_ANY_RAW(str, STR_DEPENDANT_MASK))) {
2700+
if (RB_UNLIKELY(FL_ANY_RAW(str, STR_DEPENDANT_MASK) || ruby_ractor_warn_frozen_error_objects_enabled)) {
26982701
str_modifiable(str);
26992702
return !str_dependent_p(str);
27002703
}

0 commit comments

Comments
 (0)