Skip to content

Commit 309da77

Browse files
Add support for Process.fork within an active scheduler.
1 parent 9cbf6ad commit 309da77

3 files changed

Lines changed: 98 additions & 11 deletions

File tree

lib/async/fork_handler.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
module Async
7+
# Private module that hooks into Process._fork to handle fork events.
8+
module ForkHandler
9+
def _fork(&block)
10+
if block_given?
11+
super do
12+
# Child process:
13+
if scheduler = Fiber.scheduler
14+
scheduler.process_fork if scheduler.respond_to?(:process_fork)
15+
end
16+
17+
yield
18+
end
19+
else
20+
pid = super
21+
22+
if pid == 0
23+
# Child process:
24+
if scheduler = Fiber.scheduler
25+
scheduler.process_fork if scheduler.respond_to?(:process_fork)
26+
end
27+
end
28+
29+
return pid
30+
end
31+
end
32+
end
33+
34+
private_constant :ForkHandler
35+
36+
# Hook into Process._fork to handle fork events automatically:
37+
::Process.singleton_class.prepend(ForkHandler)
38+
end

lib/async/scheduler.rb

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_relative "clock"
1010
require_relative "task"
1111
require_relative "timeout"
12+
require_relative "fork_handler"
1213

1314
require "io/event"
1415

@@ -146,24 +147,26 @@ def terminate
146147
# Terminate all child tasks and close the scheduler.
147148
# @public Since *Async v1*.
148149
def close
149-
self.run_loop do
150-
until self.terminate
151-
self.run_once!
150+
unless @children.nil?
151+
self.run_loop do
152+
until self.terminate
153+
self.run_once!
154+
end
152155
end
153156
end
154157

155158
Kernel.raise "Closing scheduler with blocked operations!" if @blocked > 0
156159
ensure
157160
# We want `@selector = nil` to be a visible side effect from this point forward, specifically in `#interrupt` and `#unblock`. If the selector is closed, then we don't want to push any fibers to it.
158-
selector = @selector
159-
@selector = nil
160-
161-
selector&.close
162-
163-
worker_pool = @worker_pool
164-
@worker_pool = nil
161+
if selector = @selector
162+
@selector = nil
163+
selector.close
164+
end
165165

166-
worker_pool&.close
166+
if worker_pool = @worker_pool
167+
@worker_pool = nil
168+
worker_pool.close
169+
end
167170

168171
consume
169172
end
@@ -642,5 +645,27 @@ def timeout_after(duration, exception, message, &block)
642645
yield duration
643646
end
644647
end
648+
649+
# Handle fork in the child process. This method is called automatically when Process.fork is invoked.
650+
#
651+
# This method:
652+
# - Terminates all tasks forcefully (without raising exceptions)
653+
# - Closes the selector (kernel state doesn't survive fork)
654+
# - Resets scheduler state
655+
# - Closes worker pool if present
656+
# - Unsets the scheduler from Fiber.scheduler
657+
#
658+
# The child process starts with a clean slate - no scheduler is set.
659+
# Users can create a new scheduler if needed.
660+
#
661+
# @public Since *Async v2*.
662+
def process_fork
663+
@children = nil
664+
@selector = nil
665+
@timers = nil
666+
667+
# Close the scheduler:
668+
Fiber.set_scheduler(nil)
669+
end
645670
end
646671
end

test/process/fork.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "sus/fixtures/async"
7+
require "async"
8+
9+
describe Process do
10+
describe ".fork" do
11+
it "can fork with block form" do
12+
r, w = IO.pipe
13+
14+
Async do
15+
Process.fork do
16+
w.write("hello")
17+
end
18+
19+
w.close
20+
expect(r.read).to be == "hello"
21+
end
22+
end
23+
end
24+
end

0 commit comments

Comments
 (0)