mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
Fix sending backtraces across ractors
[Bug #21818] Currently exceptions can be sent across ractors, but because of a limitation in the TypedData API, the exception backtrace is duped as an empty backtrace. The problem is that backtraces are embedded objects, hence the classic `rb_class_alloc(klass)` API is insufficient because we need to know the size of the Backtrace object we're duping to instantiate the copy. This is worked around by changing Ractors to call `#clone` on objects rather than use `rb_obj_clone`, and to implement `Thread::Backtrace#clone` to properly clone the variable size object.
This commit is contained in:
1 parent
3f1acedb62
commit
9a2ffd8e54
7 files changed
+140
-17
Notes:
git
2026-04-01 10:44:23 +00:00
No files matched your search
@@ -507,7 +507,7 @@ assert_equal 'false', %q{
|
||||
}
|
||||
|
||||
# To copy the object, now Marshal#dump is used
|
||||
assert_match /can not copy unshareable object/, %q{
|
||||
assert_match /can't clone unshareable instance of Thread/, %q{
|
||||
obj = Thread.new{}
|
||||
begin
|
||||
r = Ractor.new obj do |msg|
|
||||
|
||||
@@ -28,6 +28,7 @@ firstline, predefined = __LINE__+1, %[\
|
||||
send
|
||||
__send__
|
||||
__recursive_key__
|
||||
clone
|
||||
initialize
|
||||
initialize_copy
|
||||
initialize_clone
|
||||
|
||||
@@ -19980,6 +19980,7 @@ vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/compilers.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/error.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/gc.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/imemo.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/object.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/serial.h
|
||||
vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/set_table.h
|
||||
|
||||
@@ -2066,6 +2066,31 @@ ractor_move(VALUE obj)
|
||||
}
|
||||
}
|
||||
|
||||
static VALUE
|
||||
ractor_call_clone_try(VALUE obj)
|
||||
{
|
||||
return rb_funcall(obj, idClone, 0);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
ractor_call_clone_rescue(VALUE obj, VALUE exc)
|
||||
{
|
||||
rb_raise(rb_eRactorError, "can't clone unshareable instance of %"PRIsVALUE, rb_class_of(obj));
|
||||
UNREACHABLE_RETURN(Qnil);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
ractor_obj_clone(VALUE obj)
|
||||
{
|
||||
VALUE clone = rb_rescue(ractor_call_clone_try, obj, ractor_call_clone_rescue, obj);
|
||||
|
||||
if (obj == clone) {
|
||||
rb_raise(rb_eRactorError, "#clone returned self");
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
static enum obj_traverse_iterator_result
|
||||
copy_enter(VALUE obj, struct obj_traverse_replace_data *data)
|
||||
{
|
||||
@@ -2074,10 +2099,7 @@ copy_enter(VALUE obj, struct obj_traverse_replace_data *data)
|
||||
return traverse_skip;
|
||||
}
|
||||
else {
|
||||
if (!rb_get_alloc_func(rb_obj_class(obj))) {
|
||||
rb_raise(rb_eRactorError, "can not copy unshareable object %+"PRIsVALUE, obj);
|
||||
}
|
||||
data->replacement = rb_obj_clone(obj);
|
||||
data->replacement = ractor_obj_clone(obj);
|
||||
return traverse_cont;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
%# -*- c -*-
|
||||
/* DO NOT EDIT THIS FILE DIRECTLY */
|
||||
/* DO NOT EDIT THIS FILE DIRECTLY: source is at template/id.c.tmpl */
|
||||
/**********************************************************************
|
||||
|
||||
id.c -
|
||||
|
||||
@@ -50,6 +50,57 @@ class TestRactor < Test::Unit::TestCase
|
||||
assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()> because it refers unshareable objects", exception: Ractor::Error)
|
||||
end
|
||||
|
||||
def test_sending_exception_with_backtrace
|
||||
assert_ractor(<<~'RUBY')
|
||||
def build_error
|
||||
raise "Test"
|
||||
rescue => error
|
||||
error
|
||||
end
|
||||
|
||||
error = build_error
|
||||
refute_empty error.backtrace
|
||||
refute_empty error.backtrace_locations
|
||||
|
||||
backtrace, backtrace_locations = Ractor.new(error) do |error2|
|
||||
[error2.backtrace, error2.backtrace_locations]
|
||||
end.value
|
||||
|
||||
assert_equal error.backtrace, backtrace
|
||||
refute_empty backtrace_locations
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_sending_exception_with_array_backtrace
|
||||
assert_ractor(<<~'RUBY')
|
||||
error = StandardError.new
|
||||
error.set_backtrace(["foo", "bar"])
|
||||
refute_empty error.backtrace
|
||||
assert_nil error.backtrace_locations
|
||||
|
||||
backtrace, backtrace_locations = Ractor.new(error) do |error2|
|
||||
[error2.backtrace, error2.backtrace_locations]
|
||||
end.value
|
||||
|
||||
assert_equal error.backtrace, backtrace
|
||||
assert_nil backtrace_locations
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_sending_object_with_broken_clone
|
||||
assert_ractor(<<~'RUBY')
|
||||
o = Object.new
|
||||
def o.clone
|
||||
self
|
||||
end
|
||||
ractor = Ractor.new { Ractor.receive }
|
||||
error = assert_raise Ractor::Error do
|
||||
ractor.send(o)
|
||||
end
|
||||
assert_match "#clone returned self", error.message
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_default_thread_group
|
||||
assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
|
||||
begin;
|
||||
@@ -219,7 +270,7 @@ class TestRactor < Test::Unit::TestCase
|
||||
err = assert_raise(Ractor::Error) do
|
||||
Ractor.new(pr) {}.join
|
||||
end
|
||||
assert_match(/can not copy unshareable object/, err.message)
|
||||
assert_match(/can not copy Proc object/, err.message)
|
||||
RUBY
|
||||
end
|
||||
|
||||
|
||||
+58
-10
@@ -13,6 +13,7 @@
|
||||
#include "internal.h"
|
||||
#include "internal/class.h"
|
||||
#include "internal/error.h"
|
||||
#include "internal/object.h"
|
||||
#include "internal/vm.h"
|
||||
#include "iseq.h"
|
||||
#include "ruby/debug.h"
|
||||
@@ -576,14 +577,6 @@ rb_backtrace_p(VALUE obj)
|
||||
return rb_typeddata_is_kind_of(obj, &backtrace_data_type);
|
||||
}
|
||||
|
||||
static VALUE
|
||||
backtrace_alloc(VALUE klass)
|
||||
{
|
||||
rb_backtrace_t *bt;
|
||||
VALUE obj = TypedData_Make_Struct(klass, rb_backtrace_t, &backtrace_data_type, bt);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
backtrace_alloc_capa(long num_frames, rb_backtrace_t **backtrace)
|
||||
{
|
||||
@@ -962,6 +955,46 @@ backtrace_limit(VALUE self)
|
||||
return LONG2NUM(rb_backtrace_length_limit);
|
||||
}
|
||||
|
||||
/* :nodoc: */
|
||||
static VALUE
|
||||
backtrace_clone(VALUE self)
|
||||
{
|
||||
rb_backtrace_t *bt;
|
||||
TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt);
|
||||
|
||||
rb_backtrace_t *other_bt;
|
||||
VALUE clone = backtrace_alloc_capa(bt->backtrace_size, &other_bt);
|
||||
|
||||
rb_obj_clone_setup(self, clone, Qfalse);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/* :nodoc: */
|
||||
static VALUE
|
||||
backtrace_dup(VALUE self)
|
||||
{
|
||||
rb_notimplement();
|
||||
|
||||
UNREACHABLE_RETURN(Qnil);
|
||||
}
|
||||
|
||||
/* :nodoc: */
|
||||
static VALUE
|
||||
backtrace_initialize_copy(VALUE self, VALUE original)
|
||||
{
|
||||
rb_backtrace_t *bt;
|
||||
TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt);
|
||||
|
||||
rb_backtrace_t *original_bt;
|
||||
TypedData_Get_Struct(original, rb_backtrace_t, &backtrace_data_type, original_bt);
|
||||
|
||||
bt->backtrace_size = original_bt->backtrace_size;
|
||||
MEMCPY(bt->backtrace, original_bt->backtrace, rb_backtrace_location_t, original_bt->backtrace_size);
|
||||
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
VALUE
|
||||
rb_ec_backtrace_str_ary(const rb_execution_context_t *ec, long lev, long n)
|
||||
{
|
||||
@@ -1406,6 +1439,14 @@ each_caller_location(int argc, VALUE *argv, VALUE _)
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
backtrace_no_allocator(VALUE klass)
|
||||
{
|
||||
rb_notimplement();
|
||||
|
||||
UNREACHABLE_RETURN(Qnil);
|
||||
}
|
||||
|
||||
/* called from Init_vm() in vm.c */
|
||||
void
|
||||
Init_vm_backtrace(void)
|
||||
@@ -1416,11 +1457,18 @@ Init_vm_backtrace(void)
|
||||
* settings of the current session.
|
||||
*/
|
||||
rb_cBacktrace = rb_define_class_under(rb_cThread, "Backtrace", rb_cObject);
|
||||
rb_define_alloc_func(rb_cBacktrace, backtrace_alloc);
|
||||
rb_undef_method(CLASS_OF(rb_cBacktrace), "new");
|
||||
|
||||
// Can't undefine the allocator, as it's needed as a key by Marshal
|
||||
rb_define_alloc_func(rb_cBacktrace, backtrace_no_allocator);
|
||||
rb_marshal_define_compat(rb_cBacktrace, rb_cArray, backtrace_dump_data, backtrace_load_data);
|
||||
|
||||
rb_undef_method(CLASS_OF(rb_cBacktrace), "new");
|
||||
rb_define_singleton_method(rb_cBacktrace, "limit", backtrace_limit, 0);
|
||||
|
||||
rb_define_method(rb_cBacktrace, "clone", backtrace_clone, 0);
|
||||
rb_define_method(rb_cBacktrace, "dup", backtrace_dup, 0);
|
||||
rb_define_method(rb_cBacktrace, "initialize_copy", backtrace_initialize_copy, 1);
|
||||
|
||||
/*
|
||||
* An object representation of a stack frame, initialized by
|
||||
* Kernel#caller_locations.
|
||||
|
||||
Reference in new issue
Block a user