Bug #22122
closedA Ractor/Ractor::Port memory leak (or so it would seem)
Description
Hi!
If I do something like this:
#!/usr/bin/env ruby
1000.times do
return_port = Ractor::Port.new
ractor = Ractor.new(return_port) do |return_port|
return_port << Ractor::Port.new while receive
end
10_000.times { ractor << true }
# If you uncomment this then the leak goes away:
# 10_000.times { return_port.receive }
ractor << nil
ractor.join
end
GC.start
ractor_count = 0
port_count = 0
ObjectSpace.each_object do
ractor_count += 1 if Ractor === it
port_count += 1 if Ractor::Port === it
end
puts "ractors: #{ractor_count} ports: #{port_count}"
Then I see:
Note that you can also just make the number of loops super big or infinite and just watch the memory grow.
But a possible hint: if I uncomment that line that pulls stuff out of the ports it's creating, then I get:
Which is what I would expect since nothing references any of the ractors or ports created by the script and the ractors have all terminated.
I can reproduce in both:
ruby 4.1.0dev (2026-06-19T18:04:54Z master 1c1dafa769) +PRISM [x86_64-linux]
and
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
Attached is the script.
Thanks!
Files
Updated by luke-gru (Luke Gruber) 3 months ago
- Assignee set to ractor
Updated by ko1 (Koichi Sasada) 26 days ago
- Status changed from Open to Closed
Applied in changeset git|ae5ca459442a22ab5966e1aff644e18473f87369.
Free messages addressed to a port that died before delivery
A message sent to a Ractor::Port is enqueued on the receiving Ractor's
recv_queue, and only moved to that port's own queue when the Ractor
receives or closes. rb_ractor_reap_dead_ports() sweeps the port queues
held in sync.ports, so a message whose port became unreachable before any
delivery happened was never reaped: it stayed on recv_queue, off-heap and
invisible to ObjectSpace, for the life of the process.
grew without bound (RSS 858 -> 1692 -> 2523 MB over three such passes),
and needs no Ractor at all to hit. Closing the port, or any receive on
the same Ractor, moved the messages to the port queue and hid the leak.
Sweep recv_queue in the reap as well, dropping the baskets whose port is
no longer in the table. The reap takes no sync lock: what keeps out the
foreign senders that write recv_queue is the caller's gate in
rb_ractor_finish_marking(), where a global GC has the world stopped and a
single objspace has exactly one live Ractor. Assert that at the callee,
so a second caller cannot lose the guarantee silently.
[Bug #22122]
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com