Skip to content

Commit de57cf7

Browse files
samuel-williams-shopifyioquatix
authored andcommitted
Add support for Process.fork within an active scheduler.
1 parent ea8b072 commit de57cf7

File tree

5 files changed

+110
-12
lines changed

5 files changed

+110
-12
lines changed

lib/async/fork_handler.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
result = super
11+
12+
if result.zero?
13+
# Child process:
14+
if Fiber.scheduler.respond_to?(:process_fork)
15+
Fiber.scheduler.process_fork
16+
end
17+
end
18+
19+
return result
20+
end
21+
end
22+
23+
private_constant :ForkHandler
24+
25+
# Hook into Process._fork to handle fork events automatically:
26+
::Process.singleton_class.prepend(ForkHandler)
27+
end

lib/async/node.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ def parent=(parent)
214214
end
215215

216216
protected def remove_child(child)
217-
@children.remove(child)
217+
# In the case of a fork, the children list may be nil:
218+
@children&.remove(child)
218219
child.set_parent(nil)
219220
end
220221

lib/async/scheduler.rb

Lines changed: 30 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,21 @@ 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+
# The child process starts with a clean slate - no scheduler is set. Users can create a new scheduler if needed.
652+
#
653+
# @public Since *Async v2.35*.
654+
def process_fork
655+
@profiler&.stop
656+
657+
@children = nil
658+
@selector = nil
659+
@timers = nil
660+
661+
# Close the scheduler:
662+
Fiber.set_scheduler(nil)
663+
end
645664
end
646665
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- `Process.fork` is now properly handled by the Async fiber scheduler, ensuring that the scheduler state is correctly reset in the child process after a fork. This prevents issues where the child process inherits the scheduler state from the parent, which could lead to unexpected behavior.
6+
37
## v2.34.0
48

59
### `Kernel::Barrier` Convenience Interface

test/process/fork.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
pid = Process.fork do
16+
# Child process:
17+
w.write("hello")
18+
end
19+
20+
# Parent process:
21+
w.close
22+
expect(r.read).to be == "hello"
23+
ensure
24+
Process.waitpid(pid) if pid
25+
end
26+
end
27+
28+
it "can fork with non-block form" do
29+
r, w = IO.pipe
30+
31+
Async do
32+
unless pid = Process.fork
33+
# Child process:
34+
w.write("hello")
35+
36+
exit!
37+
end
38+
39+
# Parent process:
40+
w.close
41+
expect(r.read).to be == "hello"
42+
ensure
43+
Process.waitpid(pid) if pid
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)