Skip to content

Commit 76c2402

Browse files
Use async-signals for controller traps.
Assisted-By: devx/3236e566-7538-432e-a30a-2bdf37265ed4
1 parent db74350 commit 76c2402

8 files changed

Lines changed: 94 additions & 244 deletions

File tree

async-container.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
2525
spec.required_ruby_version = ">= 3.3"
2626

2727
spec.add_dependency "async", "~> 2.22"
28+
spec.add_dependency "async-signals", "~> 0.1"
2829
end

lib/async/container.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# Copyright, 2017-2025, by Samuel Williams.
55

66
require_relative "container/events"
7-
require_relative "container/signals"
87
require_relative "container/controller"
98

109
# @namespace

lib/async/container/controller.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
require_relative "notify"
1111
require_relative "policy"
1212
require_relative "events"
13-
require_relative "signals"
13+
14+
require "async/signals"
1415

1516
module Async
1617
module Container
@@ -29,6 +30,25 @@ module Container
2930
# Manages the life-cycle of one or more containers in order to support a persistent system.
3031
# e.g. a web server, job server or some other long running system.
3132
class Controller
33+
# Represents a trapped process signal as a queued controller event.
34+
class SignalEvent
35+
# Initialize the signal event.
36+
# @parameter signal [Symbol | String | Integer] The signal that was received.
37+
# @parameter handler [Proc] The handler to invoke when the event is processed.
38+
def initialize(signal, handler)
39+
@signal = signal
40+
@handler = handler
41+
end
42+
43+
# @attribute [Symbol | String | Integer] The signal that was received.
44+
attr :signal
45+
46+
# Process the signal event by invoking the registered handler.
47+
def call
48+
@handler.call
49+
end
50+
end
51+
3252
SIGHUP = Signal.list["HUP"]
3353
SIGINT = Signal.list["INT"]
3454
SIGTERM = Signal.list["TERM"]
@@ -44,7 +64,7 @@ def initialize(notify: Notify.open!, container_class: Container, graceful_stop:
4464

4565
@container = nil
4666
@events = Events.new
47-
@signals = Signals.new(@events)
67+
@signals = Async::Signals::Handlers.new
4868

4969
self.trap(SIGHUP) do
5070
self.restart
@@ -101,7 +121,15 @@ def to_s
101121
# @parameters signal [Symbol] The signal to trap, e.g. `:INT`.
102122
# @parameters block [Proc] The signal handler to invoke.
103123
def trap(signal, &block)
104-
@signals.trap(signal, &block)
124+
if block
125+
event = SignalEvent.new(signal, block).freeze
126+
127+
@signals.trap(signal) do
128+
@events << event
129+
end
130+
else
131+
@signals.ignore(signal)
132+
end
105133
end
106134

107135
# Create a policy for managing child lifecycle events.
@@ -247,7 +275,7 @@ def reload
247275
def run
248276
@notify&.status!("Initializing controller...")
249277

250-
@signals.trapped do
278+
Async::Signals.install(@signals) do
251279
self.start
252280

253281
while event = @events.pop(timeout: 0)

lib/async/container/signals.rb

Lines changed: 0 additions & 93 deletions
This file was deleted.

releases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
- Add `Async::Container::Signals` for installing scoped signal traps that enqueue signal events.
5+
- Use `async-signals` to coordinate controller signal traps while queueing signal events through the controller event loop.
66

77
## v0.37.0
88

test/async/container/controller.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,27 @@ def controller.setup(container)
210210
with "signals" do
211211
include_context Async::Container::AController, "dots"
212212

213+
it "queues trapped signal events" do
214+
controller = Async::Container::Controller.new(notify: nil)
215+
applied = false
216+
217+
controller.trap(:USR1) do
218+
applied = true
219+
end
220+
221+
Async::Signals.install(controller.instance_variable_get(:@signals)) do
222+
Process.kill(:USR1, Process.pid)
223+
224+
event = controller.instance_variable_get(:@events).pop(timeout: 1)
225+
226+
expect(event.signal).to be == :USR1
227+
228+
event.call
229+
end
230+
231+
expect(applied).to be == true
232+
end
233+
213234
it "restarts children when receiving SIGHUP" do
214235
expect(input.read(1)).to be == "."
215236

test/async/container/events.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2026, by Samuel Williams.
5+
6+
require "async/container/controller"
7+
8+
describe Async::Container::Controller::SignalEvent do
9+
it "calls the handler" do
10+
applied = false
11+
12+
event = subject.new(:USR1, proc{applied = true})
13+
14+
expect(event.signal).to be == :USR1
15+
16+
event.call
17+
18+
expect(applied).to be == true
19+
end
20+
end
21+
22+
describe Async::Container::Events do
23+
let(:events) {subject.new}
24+
25+
it "wakes IO.select when an event is queued" do
26+
event = Object.new
27+
28+
events << event
29+
30+
readable, _, _ = IO.select([events.io], nil, nil, 0)
31+
32+
expect(readable).to be == [events.io]
33+
expect(events.pop(timeout: 0)).to be == event
34+
end
35+
36+
it "returns nil when no event is queued" do
37+
expect(events.pop(timeout: 0)).to be_nil
38+
end
39+
end

0 commit comments

Comments
 (0)