Skip to content

Commit 587d588

Browse files
Add failing test for fork + process exit Tokio panic
Reproduces temporalio/samples-ruby#78: when a process forks after a Temporal runtime is created (e.g., Rails parallelize) and the child exits with `exit` (triggering Ruby's object finalization), the Rust Drop for CoreRuntime tries to shut down Tokio, which wakes the I/O driver using inherited (now invalid) kqueue/epoll FDs — causing a panic: "failed to wake I/O driver: Bad file descriptor". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> squash
1 parent 1cda7a1 commit 587d588

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

temporalio/rbs_collection.lock.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ gems:
153153
version: '0'
154154
source:
155155
type: stdlib
156+
- name: prism
157+
version: 1.9.0
158+
source:
159+
type: rubygems
156160
- name: pstore
157161
version: '0'
158162
source:

temporalio/test/client_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,41 @@ def test_fork
235235
assert_equal 'started workflow', reader.read.strip
236236
end
237237

238+
def test_fork_with_runtime_gc
239+
# When a process forks after a Temporal runtime is created, the child
240+
# inherits the Tokio runtime's kqueue/epoll FDs.
241+
# If the Runtime's Rust struct is freed (via GC or process exit), the
242+
# CoreRuntime Drop tries to shut down Tokio, which wakes the I/O driver
243+
# using the inherited (now invalid) FDs — causing a panic:
244+
# "failed to wake I/O driver: Bad file descriptor".
245+
#
246+
# We throw errors explaining Temporal objects cannot be used across forks,
247+
# but this would be obscured by the panic on fork exit.
248+
#
249+
# We create a standalone runtime, fork, null the reference in the child,
250+
# and force GC to trigger extern_free on the inherited object.
251+
pre_fork_runtime = Temporalio::Runtime.new
252+
253+
reader, writer = IO.pipe
254+
pid = fork do
255+
reader.close
256+
# Drop the only reference and force GC to free the inherited Runtime.
257+
# Without the fork-safe free, this triggers a Tokio panic.
258+
pre_fork_runtime = nil
259+
GC.start
260+
writer.puts('child-ok')
261+
exit! 0
262+
rescue StandardError => e
263+
writer.puts("child-error: #{e.message}")
264+
exit! 1
265+
end
266+
_, status = Process.wait2(pid)
267+
writer.close
268+
result = reader.read.strip
269+
refute status.signaled?, "Child killed by signal #{status.termsig} (likely Tokio I/O driver panic)"
270+
assert_equal 'child-ok', result
271+
end
272+
238273
def test_binary_metadata
239274
orig_metadata = env.client.connection.rpc_metadata
240275

0 commit comments

Comments
 (0)