From cc57f9c81852b37ea3bc2f29359bdf3237c2ab6d Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Wed, 15 Jul 2026 00:24:33 +0200 Subject: [PATCH] Include ivar name and class name in the Ractor isolation error message - ### Problem When a Ractor isolation error happens due to setting an ivar on a class, the message doesn't include the name of the ivar and neither the name of the class. It makes it difficult to track down where the isolation problem comes from. I'd like the message to be similar as what we get when a Ractor access an unshareable ivar. This is the current behaviour ```ruby class Foo def self.foo @foo ||= "test" end end Ractor.new do Foo.foo # can not set instance variables of classes/modules by non-main Ractors end.join Foo.foo Ractor.new do Foo.foo # can not get unshareable values from instance variables of classes/modules from non-main Ractors (@foo from Foo) end.join ``` ### Solution Modify the error message to include the ivar name and the class/module name. Co-authored-by: Yaroslav Markin --- bootstraptest/test_ractor.rb | 85 ++++++++++++++++++++++++++++++++++++ variable.c | 19 ++++++-- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 51d2762d939..cedc73da789 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -888,6 +888,91 @@ assert_equal "can not get unshareable values from instance variables of classes/ end RUBY +# setting an ivar of a class/module from a non-owner Ractor reports the ivar and class +assert_equal "can not set instance variables of classes/modules created by another Ractor (@iv from C)", <<~'RUBY', frozen_string_literal: false + class C + @iv = 'str' + end + + r = Ractor.new do + class C + @iv = 'other' + end + end + + begin + r.value + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY + +# a non-main Ractor can read, set, and remove ivars of classes/modules it created +assert_equal 'ok', <<~'RUBY', frozen_string_literal: false + Ractor.new do + [Class.new, Module.new].each do |mod| + mod.instance_variable_set(:@iv, 'str') + raise unless mod.instance_variable_get(:@iv) == 'str' + raise unless mod.remove_instance_variable(:@iv) == 'str' + end + :ok + end.value +RUBY + +# even main cannot modify another Ractor's module; reporting its name must not call to_s +assert_equal 'true', <<~'RUBY', frozen_string_literal: false + port = Ractor::Port.new + r = Ractor.new(port) do |port| + mod = Module.new + mod.set_temporary_name('worker_module') + mod.instance_variable_set(:@iv, 'str') + def mod.to_s = raise('to_s must not be called') + port << mod + Ractor.receive + end + mod = port.receive + + begin + messages = [[:instance_variable_set, :@iv, 'other'], [:remove_instance_variable, :@iv]].map do |args| + begin + mod.public_send(*args) + rescue Ractor::IsolationError => e + e.message + end + end + messages == ["can not set instance variables of classes/modules created by another Ractor (@iv from worker_module)"] * 2 + ensure + r << nil + r.join + end +RUBY + +# normal and JIT-compiled ivar reads report the same names without calling to_s +assert_equal 'true', <<~'RUBY', frozen_string_literal: false + class C + @iv = 'str' + def self.read = @iv + def self.to_s = raise('to_s must not be called') + end + + Ractor.new do + messages = [] + begin + C.instance_variable_get(:@iv) + rescue Ractor::IsolationError => e + messages << e.message + end + 40.times do + begin + C.read + rescue Ractor::IsolationError => e + messages << e.message + end + end + messages == ["can not get unshareable values from instance variables of classes/modules created by another Ractor (@iv from C)"] * 41 + end.value +RUBY + # setting an ivar on a shareable but unfrozen object is not allowed, by instance_variable_set assert_equal "can't modify instance variables of a shareable Ractor", %q{ shared = Ractor.new{} diff --git a/variable.c b/variable.c index 5d6e1b17637..e23c5056f51 100644 --- a/variable.c +++ b/variable.c @@ -1274,7 +1274,13 @@ class_ivar_set_ractor_check(VALUE klass, ID id) { if (rb_is_instance_id(id) && // check only normal ivars UNLIKELY(!rb_class_owned_p(klass))) { - rb_raise(rb_eRactorIsolationError, "can not set instance variables of classes/modules created by another Ractor"); + rb_raise( + rb_eRactorIsolationError, + "can not set instance variables of classes/modules " + "created by another Ractor (%"PRIsVALUE" from %"PRIsVALUE")", + rb_id2str(id), + rb_class_path(klass) + ); } } @@ -1627,7 +1633,7 @@ rb_ivar_lookup(VALUE obj, ID id, VALUE undef) "can not get unshareable values from instance variables of classes/modules " "created by another Ractor (%"PRIsVALUE" from %"PRIsVALUE")", rb_id2str(id), - obj + rb_class_path(obj) ); } } @@ -1659,8 +1665,13 @@ rb_ivar_get_at(VALUE obj, attr_index_t index, ID id) VALUE val = rb_imemo_fields_ptr(fields_obj)[index]; if (UNLIKELY(!rb_class_owned_p(obj)) && !rb_ractor_shareable_p(val)) { - rb_raise(rb_eRactorIsolationError, - "can not get unshareable values from instance variables of classes/modules created by another Ractor"); + rb_raise( + rb_eRactorIsolationError, + "can not get unshareable values from instance variables of classes/modules " + "created by another Ractor (%"PRIsVALUE" from %"PRIsVALUE")", + rb_id2str(id), + rb_class_path(obj) + ); } return val;