Skip to content

Commit 621844e

Browse files
committed
Fix Hybrid fork respawn loop on single interrupt. Fixes #58.
1 parent 481910c commit 621844e

3 files changed

Lines changed: 47 additions & 0 deletions

File tree

lib/async/container/generic.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def wait
117117

118118
# Gracefully interrupt all child instances.
119119
def interrupt
120+
# We must enter the stopping state before signalling the children. Interrupting a child causes it to drain and exit, but the main run loop will respawn any child that exits while `restart: true` and the container is not stopping (see the `restart && !@stopping` gate in `#run`). Without setting this flag, an interrupted child immediately respawns, so the container never drains and `#wait` never returns.
121+
#
122+
# This matters most for `Hybrid` containers: a `SIGINT`/`SIGTERM` delivered to a fork is translated into a call to `#interrupt` on the inner threaded container, which typically runs with `restart: true` (the default for `async-service` managed services). If `#interrupt` did not set this flag, the inner threads would drain, exit, and respawn in a loop, so a single signal would never terminate the fork. Setting `@stopping = true` here makes `#interrupt` behave as the start of a graceful shutdown: children drain and exit, are not respawned, and the fork terminates - consistent with how `Forked` and `Threaded` containers handle a single interrupt.
123+
@stopping = true
120124
@group.interrupt
121125
end
122126

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+
- **Fixed**: `Hybrid` container now stops on interrupt instead of restarting indefinitely.
6+
37
## v0.35.0
48

59
- **Fixed**: `Hybrid` now interrupts inner threaded children during graceful shutdown and force-stops remaining children on exit.

test/async/container/hybrid.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,43 @@ def instance.ready!
6161
Async::Container.send(:remove_const, :Threaded)
6262
Async::Container.const_set(:Threaded, original_threaded)
6363
end
64+
65+
# https://github.com/socketry/async-container/issues/58
66+
it "exits the fork on a single interrupt even when the inner container has restart: true" do
67+
pids = IO.pipe
68+
69+
container = subject.new
70+
container.run(count: 1, forks: 1, threads: 1, restart: true) do |instance|
71+
pids.last.puts(Process.pid.to_s)
72+
instance.ready!
73+
sleep
74+
end
75+
76+
container.wait_until_ready
77+
78+
fork_pid = Integer(pids.first.gets)
79+
80+
# Mimic a single SIGINT delivered to the fork (e.g. memory-based worker recycling):
81+
Process.kill(:INT, fork_pid)
82+
83+
# The fork must drain its inner threads and exit, rather than respawning them forever:
84+
exited = false
85+
8.times do
86+
reaped, _status = Process.waitpid2(fork_pid, Process::WNOHANG)
87+
if reaped
88+
exited = true
89+
break
90+
end
91+
sleep(0.1)
92+
rescue Errno::ECHILD
93+
exited = true
94+
break
95+
end
96+
97+
expect(exited).to be == true
98+
ensure
99+
Process.kill(:KILL, fork_pid) if fork_pid && !exited
100+
container&.stop
101+
pids&.each(&:close)
102+
end
64103
end if Async::Container.fork?

0 commit comments

Comments
 (0)