Project

General

Profile

Actions

Bug #21818

open

Thread backtraces cannot be communicated over Ractor ports

Bug #21818: Thread backtraces cannot be communicated over Ractor ports

Added by dazuma (Daniel Azuma) about 2 hours ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25]
[ruby-core:124414]

Description

It looks like Thread::Backtrace is confined to the Ractor that produced it: it cannot be copied or moved to another Ractor, nor can it be made shareable. This makes it difficult for a Ractor to communicate exceptions (e.g. the reason for its own failure) to other Ractors. Is this intentional? I would have expected that a Thread::Backtrace is just static data that should not have problems moving between Ractors.

Details below:

Thread::Backtrace cannot be moved to another Ractor

Code:

def make_trace
  caller_locations
end

r1 = Ractor.new do
  port = receive
  trace = make_trace
  puts "**** Original: #{trace.inspect}"
  port.send(trace, move: true)  # Fails here
end

port = Ractor::Port.new
r1.send(port)
trace = port.receive  # Hangs here
puts "**** Received: #{trace.inspect}"

Result:

**** Original: ["hello.rb:7:in 'block in <main>'"]
#<Thread:0x000000010438f770 run> terminated with exception (report_on_exception is true):
hello.rb:9:in 'Ractor::Port#send': can not move Thread::Backtrace::Location object. (Ractor::Error)
	from hello.rb:9:in 'block in <main>'

As a result, it is also not possible to move an Exception:

r1 = Ractor.new do
  port = receive
  begin
    raise "hello"
  rescue => e
    port.send(e, move: true)  # Fails here
  end
end

port = Ractor::Port.new
r1.send(port)
e = port.receive  # Hangs here

Result:

#<Thread:0x0000000102a0f570 run> terminated with exception (report_on_exception is true):
hello.rb:6:in 'Ractor::Port#send': can not move Thread::Backtrace object. (Ractor::Error)
	from hello.rb:6:in 'block in <main>'

Note that Kernel#caller returns a string array that can be moved successfully:

def make_trace
  caller
end

r1 = Ractor.new do
  port = receive
  trace = make_trace
  puts "**** Original: #{trace.inspect}"
  port.send(trace, move: true)
end

port = Ractor::Port.new
r1.send(port)
trace = port.receive
puts "**** Received: #{trace.inspect}"

Result:

**** Original: ["hello.rb:7:in 'block in <main>'"]
**** Received: ["hello.rb:7:in 'block in <main>'"]

Thread::Backtrace cannot be copied to another Ractor either

Code:

def make_trace
  caller_locations
end

r1 = Ractor.new do
  port = receive
  trace = make_trace
  puts "**** Original: #{trace.inspect}"
  port.send(trace, move: false)  # Fails here
end

port = Ractor::Port.new
r1.send(port)
trace = port.receive  # Hangs here
puts "**** Received: #{trace.inspect}"

Result. Note the specific error message "allocator undefined" that might give a clue why this is all happening:

**** Original: ["hello.rb:7:in 'block in <main>'"]
#<Thread:0x000000010031f5c8 run> terminated with exception (report_on_exception is true):
hello.rb:9:in 'Ractor::Port#send': allocator undefined for Thread::Backtrace::Location (TypeError)

  port.send(trace, move: false)
            ^^^^^^^^^^^^^^^^^^
	from hello.rb:9:in 'block in <main>'

Again, Kernel#caller returns a string array that copies successfully:

def make_trace
  caller
end

r1 = Ractor.new do
  port = receive
  trace = make_trace
  puts "**** Original: #{trace.inspect}"
  port.send(trace, move: false)  # Fails here
end

port = Ractor::Port.new
r1.send(port)
trace = port.receive  # Hangs here
puts "**** Received: #{trace.inspect}"

Result:

**** Original: ["hello.rb:7:in 'block in <main>'"]
**** Received: ["hello.rb:7:in 'block in <main>'"]

Interestingly, if an exception is copied when sent over a port, it doesn't fail with an undefined allocator, but the backtrace in the copy is empty:

r1 = Ractor.new do
  port = receive
  begin
    raise "hello"
  rescue => e
    puts "**** Original: #{e.backtrace.inspect}"
    port.send(e, move: false)
  end
end

port = Ractor::Port.new
r1.send(port)
e = port.receive
puts "**** Received: #{e.backtrace.inspect}"

Result:

**** Original: ["hello.rb:4:in 'block in <main>'"]
**** Received: []

Thread::Backtrace cannot be made shareable

Code:

def make_trace
  caller_locations
end

trace = Ractor.make_shareable(make_trace)  # Fails here

Result:

hello.rb:5:in 'Ractor.make_shareable': can not make shareable object for "hello.rb:5:in '<main>'" (Ractor::Error)
	from hello.rb:5:in '<main>'

As a result, exceptions also cannot be made shareable:

begin
  raise "hello"
rescue => e
  e = Ractor.make_shareable(e)  # Fails here
end

Result:

hello.rb: can not make shareable object for #<Thread::Backtrace:0x0000000102fdfe28> (Ractor::Error)

Interestingly, if the backtrace is empty, make_shareable succeeds, suggesting that it's specifically the Thread::Backtrace::Location that cannot be made shareable:

trace = Ractor.make_shareable(caller_locations)
puts trace.inspect

Result:

[]

And, of course, the result of Kernel#caller can be made shareable:

def make_trace
  caller
end

trace = Ractor.make_shareable(make_trace)
puts trace.inspect

Result:

["hello.rb:5:in '<main>'"]

No data to display

Actions

Also available in: PDF Atom