Skip to content

Commit 8686b5c

Browse files
Select default signals only at top level. (#5)
1 parent 2b760ed commit 8686b5c

4 files changed

Lines changed: 91 additions & 8 deletions

File tree

guides/getting-started/readme.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ The installed handlers are snapshotted when they are installed. Later changes to
139139

140140
### Choosing a Signal Backend
141141

142-
Use {ruby Async::Signals.default} when a component should install process signal handlers only while running on the main thread. It returns {ruby Async::Signals} on the main thread and {ruby Async::Signals::Ignore} on other threads.
142+
Use {ruby Async::Signals.default} when a component should install process signal handlers only when it appears to own the process signal boundary. It returns {ruby Async::Signals} on the main thread when no fiber scheduler is installed, and {ruby Async::Signals::Ignore} otherwise.
143143

144144
```ruby
145145
require "async/signals"
@@ -150,13 +150,53 @@ handlers.trap(:TERM) do
150150
end
151151

152152
Async::Signals.default.install(handlers) do
153-
# Process signal handlers are active only on the main thread.
153+
# Process signal handlers are active only when using the default signal backend.
154154
sleep
155155
end
156156
```
157157

158158
Use {ruby Async::Signals::Ignore} directly when a component is controlled by its parent and should not subscribe to process-wide signals.
159159

160+
### Using Signals with Async
161+
162+
When a component runs inside an existing Async event loop, it should not implicitly take ownership of process-wide signals. In that case, {ruby Async::Signals.default} returns {ruby Async::Signals::Ignore}, so installing handlers through the default backend becomes a no-op.
163+
164+
```ruby
165+
require "async"
166+
require "async/signals"
167+
168+
handlers = Async::Signals::Handlers.new
169+
handlers.trap(:TERM) do
170+
puts "Stopping..."
171+
end
172+
173+
Async do
174+
Async::Signals.default.install(handlers) do
175+
# No process signal traps are installed here.
176+
sleep
177+
end
178+
end
179+
```
180+
181+
If a component running inside an Async event loop is intended to own process signal handling, pass {ruby Async::Signals} explicitly instead of using the default backend.
182+
183+
```ruby
184+
require "async"
185+
require "async/signals"
186+
187+
handlers = Async::Signals::Handlers.new
188+
handlers.trap(:TERM) do |signal, context|
189+
context.raise(Interrupt)
190+
end
191+
192+
Async do
193+
Async::Signals.install(handlers) do
194+
# Process signal traps are explicitly installed here.
195+
sleep
196+
end
197+
end
198+
```
199+
160200
## Forking
161201

162202
Signal traps are inherited across `fork`. On Ruby implementations that support `Process._fork`, `async-signals` automatically resets inherited signal state in the forked child so the child does not keep handler registrations from the parent process.

lib/async/signals.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ def self.controller
2020
CONTROLLER
2121
end
2222

23-
# The default signal backend for the current thread.
23+
# The default signal backend for the current context.
2424
# @returns [Async::Signals | Async::Signals::Ignore] The default signal backend.
2525
def self.default
2626
if ::Thread.current == ::Thread.main
27-
self
28-
else
29-
Ignore
27+
# TruffleRuby does not currently expose `Fiber.scheduler`:
28+
unless ::Fiber.respond_to?(:scheduler) && ::Fiber.scheduler
29+
return self
30+
end
3031
end
32+
33+
return Ignore
3134
end
3235

3336
# Install signal handlers using the process-wide signal controller.

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+
- Change `Async::Signals.default` to select process signal handling only on the main thread when no fiber scheduler is installed.
6+
37
## v0.4.0
48

59
- Use `Fiber::Scheduler#fiber_interrupt` from `Context#raise` when available, falling back to `Thread#raise`.

test/async/signals.rb

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@
1111
describe Async::Signals do
1212
include Async::Signals::QueueAssertions
1313

14+
def make_scheduler
15+
Object.new.tap do |scheduler|
16+
scheduler.define_singleton_method(:block) do |*|
17+
end
18+
19+
scheduler.define_singleton_method(:unblock) do |*|
20+
end
21+
22+
scheduler.define_singleton_method(:kernel_sleep) do |*|
23+
end
24+
25+
scheduler.define_singleton_method(:io_wait) do |*|
26+
end
27+
28+
scheduler.define_singleton_method(:fiber_interrupt) do |*|
29+
end
30+
end
31+
end
32+
33+
def with_scheduler
34+
previous_scheduler = ::Fiber.scheduler
35+
::Fiber.set_scheduler(make_scheduler)
36+
37+
yield
38+
ensure
39+
::Fiber.set_scheduler(previous_scheduler)
40+
end
41+
1442
it "has a version number" do
1543
expect(subject::VERSION).to be_a(String)
1644
end
@@ -23,17 +51,25 @@
2351
end
2452

2553
with ".default" do
26-
it "returns process signals on the main thread" do
54+
it "returns process signals on the main thread without a scheduler" do
2755
expect(subject.default).to be == subject
2856
end
2957

30-
it "ignores process signals on other threads" do
58+
it "ignores process signals outside the top level" do
3159
default = ::Thread.new do
3260
subject.default
3361
end.value
3462

3563
expect(default).to be == subject::Ignore
3664
end
65+
66+
it "ignores process signals when a scheduler is installed" do
67+
skip "Fiber.set_scheduler is not supported." unless ::Fiber.respond_to?(:set_scheduler)
68+
69+
with_scheduler do
70+
expect(subject.default).to be == subject::Ignore
71+
end
72+
end
3773
end
3874

3975
with ".install" do

0 commit comments

Comments
 (0)