async-signals 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +31 -0
- data/guides/getting-started/readme.md +31 -0
- data/lib/async/signals/context.rb +25 -0
- data/lib/async/signals/controller.rb +11 -8
- data/lib/async/signals/handlers.rb +1 -0
- data/lib/async/signals/ignore.rb +31 -0
- data/lib/async/signals/version.rb +1 -1
- data/lib/async/signals.rb +12 -0
- data/readme.md +9 -0
- data/releases.md +8 -0
- data.tar.gz.sig +2 -3
- metadata +3 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 892b6d001f6a23c4190ecb71cd652d63aea1a7331932a68f3b0f057f8cdf762e
|
|
4
|
+
data.tar.gz: fd8a403c5abfe0a943ce0ec4c684ea5b6ba390f6c76006ed6772d0e12532d903
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc9458635104d6f45cdcde03512d1e5e3dcaf09f21fa345bad891f678e2a5f6d80177c5dd7f84bc11e0a2d2b4b0c4582bba5cea9871bae6aafefaf320b05b124
|
|
7
|
+
data.tar.gz: f885189f0ae71ef8c529bf17ae23c9ad080c22641716fd30e4c54c11bc0659786c030536b6448472ac9fb66667ed0736e180e0e746bfe3972cd9ad26589a8022
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/getting-started.md
CHANGED
|
@@ -25,6 +25,7 @@ Ruby signal handlers are process-wide. Calling `Signal.trap` for the same signal
|
|
|
25
25
|
- {ruby Async::Signals::Handlers} represents a configurable set of signal handlers for one consumer.
|
|
26
26
|
- {ruby Async::Signals::Controller} owns the process-wide `Signal.trap` entries while handler sets are installed.
|
|
27
27
|
- {ruby Async::Signals.install} installs a handler set using the default process-wide controller.
|
|
28
|
+
- {ruby Async::Signals::Ignore} provides a no-op signal backend for code that should not install process signal traps.
|
|
28
29
|
- {ruby Async::Signals.reset!} removes all active handlers and restores the previous signal traps.
|
|
29
30
|
|
|
30
31
|
Each handler set can trap or ignore signals independently. When multiple handler sets trap the same signal, `async-signals` installs one Ruby signal trap and dispatches the signal to each active handler.
|
|
@@ -54,6 +55,14 @@ end
|
|
|
54
55
|
|
|
55
56
|
When the block exits, the handler set is removed and any previous signal trap is restored.
|
|
56
57
|
|
|
58
|
+
Handlers may also accept the context that installed the handler set. This is useful when a signal should interrupt the component that installed the handlers, regardless of which thread dispatches the signal trap.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
handlers.trap(:INT) do |signal, context|
|
|
62
|
+
context.raise(Interrupt)
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
57
66
|
### Multiple Consumers
|
|
58
67
|
|
|
59
68
|
Multiple parts of an application can listen for the same signal. This is useful when a service, supervisor, and application component each need to observe shutdown signals without taking ownership of the process-wide trap.
|
|
@@ -128,6 +137,26 @@ end
|
|
|
128
137
|
|
|
129
138
|
The installed handlers are snapshotted when they are installed. Later changes to the handler set do not affect an existing registration.
|
|
130
139
|
|
|
140
|
+
### Choosing a Signal Backend
|
|
141
|
+
|
|
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.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require "async/signals"
|
|
146
|
+
|
|
147
|
+
handlers = Async::Signals::Handlers.new
|
|
148
|
+
handlers.trap(:TERM) do
|
|
149
|
+
puts "Stopping..."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
Async::Signals.default.install(handlers) do
|
|
153
|
+
# Process signal handlers are active only on the main thread.
|
|
154
|
+
sleep
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use {ruby Async::Signals::Ignore} directly when a component is controlled by its parent and should not subscribe to process-wide signals.
|
|
159
|
+
|
|
131
160
|
## Forking
|
|
132
161
|
|
|
133
162
|
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.
|
|
@@ -148,6 +177,8 @@ Avoid calling `Signal.trap` for the same signals while `async-signals` handlers
|
|
|
148
177
|
|
|
149
178
|
Keep signal handlers thread safe. Ruby implementations may dispatch signal traps from an implementation-specific thread, so handlers should avoid mutating shared state directly. Prefer doing minimal work in the handler and forwarding the event to a thread-safe mechanism such as `Thread::Queue`.
|
|
150
179
|
|
|
180
|
+
Handler exceptions propagate from dispatch. If multiple handler sets observe the same signal and one handler raises, later handlers may not run.
|
|
181
|
+
|
|
151
182
|
## Troubleshooting
|
|
152
183
|
|
|
153
184
|
If a handler is not invoked, check that the handler set is installed at the time the signal is delivered. Handler sets are only active inside the `Async::Signals.install` block, or until the returned registration is closed.
|
|
@@ -25,6 +25,7 @@ Ruby signal handlers are process-wide. Calling `Signal.trap` for the same signal
|
|
|
25
25
|
- {ruby Async::Signals::Handlers} represents a configurable set of signal handlers for one consumer.
|
|
26
26
|
- {ruby Async::Signals::Controller} owns the process-wide `Signal.trap` entries while handler sets are installed.
|
|
27
27
|
- {ruby Async::Signals.install} installs a handler set using the default process-wide controller.
|
|
28
|
+
- {ruby Async::Signals::Ignore} provides a no-op signal backend for code that should not install process signal traps.
|
|
28
29
|
- {ruby Async::Signals.reset!} removes all active handlers and restores the previous signal traps.
|
|
29
30
|
|
|
30
31
|
Each handler set can trap or ignore signals independently. When multiple handler sets trap the same signal, `async-signals` installs one Ruby signal trap and dispatches the signal to each active handler.
|
|
@@ -54,6 +55,14 @@ end
|
|
|
54
55
|
|
|
55
56
|
When the block exits, the handler set is removed and any previous signal trap is restored.
|
|
56
57
|
|
|
58
|
+
Handlers may also accept the context that installed the handler set. This is useful when a signal should interrupt the component that installed the handlers, regardless of which thread dispatches the signal trap.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
handlers.trap(:INT) do |signal, context|
|
|
62
|
+
context.raise(Interrupt)
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
57
66
|
### Multiple Consumers
|
|
58
67
|
|
|
59
68
|
Multiple parts of an application can listen for the same signal. This is useful when a service, supervisor, and application component each need to observe shutdown signals without taking ownership of the process-wide trap.
|
|
@@ -128,6 +137,26 @@ end
|
|
|
128
137
|
|
|
129
138
|
The installed handlers are snapshotted when they are installed. Later changes to the handler set do not affect an existing registration.
|
|
130
139
|
|
|
140
|
+
### Choosing a Signal Backend
|
|
141
|
+
|
|
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.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require "async/signals"
|
|
146
|
+
|
|
147
|
+
handlers = Async::Signals::Handlers.new
|
|
148
|
+
handlers.trap(:TERM) do
|
|
149
|
+
puts "Stopping..."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
Async::Signals.default.install(handlers) do
|
|
153
|
+
# Process signal handlers are active only on the main thread.
|
|
154
|
+
sleep
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use {ruby Async::Signals::Ignore} directly when a component is controlled by its parent and should not subscribe to process-wide signals.
|
|
159
|
+
|
|
131
160
|
## Forking
|
|
132
161
|
|
|
133
162
|
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.
|
|
@@ -148,6 +177,8 @@ Avoid calling `Signal.trap` for the same signals while `async-signals` handlers
|
|
|
148
177
|
|
|
149
178
|
Keep signal handlers thread safe. Ruby implementations may dispatch signal traps from an implementation-specific thread, so handlers should avoid mutating shared state directly. Prefer doing minimal work in the handler and forwarding the event to a thread-safe mechanism such as `Thread::Queue`.
|
|
150
179
|
|
|
180
|
+
Handler exceptions propagate from dispatch. If multiple handler sets observe the same signal and one handler raises, later handlers may not run.
|
|
181
|
+
|
|
151
182
|
## Troubleshooting
|
|
152
183
|
|
|
153
184
|
If a handler is not invoked, check that the handler set is installed at the time the signal is delivered. Handler sets are only active inside the `Async::Signals.install` block, or until the returned registration is closed.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Signals
|
|
8
|
+
# Represents the execution context that installed a signal handler set.
|
|
9
|
+
class Context
|
|
10
|
+
# Initialize the context.
|
|
11
|
+
def initialize
|
|
12
|
+
# Capture both primitives so the public interface can evolve without
|
|
13
|
+
# changing the handler arguments.
|
|
14
|
+
@thread = ::Thread.current
|
|
15
|
+
@fiber = ::Fiber.current
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raise an exception in the thread that installed the handler set.
|
|
19
|
+
# @parameter arguments [Array] The arguments to pass to {Thread#raise}.
|
|
20
|
+
def raise(*arguments)
|
|
21
|
+
@thread.raise(*arguments)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
require "thread"
|
|
7
7
|
|
|
8
8
|
require_relative "handlers"
|
|
9
|
+
require_relative "context"
|
|
9
10
|
|
|
10
11
|
module Async
|
|
11
12
|
module Signals
|
|
@@ -74,9 +75,11 @@ module Async
|
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
# The active callable signal handlers.
|
|
77
|
-
# @returns [Array(Proc)] The active handlers.
|
|
78
|
+
# @returns [Array(Array(Proc, Context))] The active handlers and the contexts that installed them.
|
|
78
79
|
def callbacks
|
|
79
|
-
@handlers.
|
|
80
|
+
@handlers.map do |registration, handler|
|
|
81
|
+
[handler, registration.context]
|
|
82
|
+
end.freeze
|
|
80
83
|
end
|
|
81
84
|
end
|
|
82
85
|
|
|
@@ -88,8 +91,12 @@ module Async
|
|
|
88
91
|
def initialize(controller, handlers)
|
|
89
92
|
@controller = controller
|
|
90
93
|
@handlers = handlers
|
|
94
|
+
@context = Context.new
|
|
91
95
|
end
|
|
92
96
|
|
|
97
|
+
# @attribute [Context] The context that installed this registration.
|
|
98
|
+
attr :context
|
|
99
|
+
|
|
93
100
|
# Remove this registration from the controller.
|
|
94
101
|
def close
|
|
95
102
|
if handlers = @handlers
|
|
@@ -138,12 +145,8 @@ module Async
|
|
|
138
145
|
def dispatch(signal)
|
|
139
146
|
number = ::Signal.list.fetch(signal)
|
|
140
147
|
|
|
141
|
-
@dispatch[signal]&.each do |handler|
|
|
142
|
-
|
|
143
|
-
handler.call(number)
|
|
144
|
-
rescue Exception => error
|
|
145
|
-
warn "Async::Signals handler failed: #{error.class}: #{error.message}"
|
|
146
|
-
end
|
|
148
|
+
@dispatch[signal]&.each do |handler, context|
|
|
149
|
+
handler.call(number, context)
|
|
147
150
|
end
|
|
148
151
|
end
|
|
149
152
|
|
|
@@ -16,6 +16,7 @@ module Async
|
|
|
16
16
|
|
|
17
17
|
# Trap a signal while these handlers are installed.
|
|
18
18
|
# @parameter signal [Symbol | String | Integer] The signal to trap.
|
|
19
|
+
# @yields {|signal, context| ...} The signal number and the context that installed the handler set.
|
|
19
20
|
def trap(signal, &block)
|
|
20
21
|
@signals[normalize(signal)] = block
|
|
21
22
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Signals
|
|
8
|
+
# Provides a no-op signal backend.
|
|
9
|
+
module Ignore
|
|
10
|
+
# Represents a no-op signal registration.
|
|
11
|
+
class Registration
|
|
12
|
+
# Close the registration.
|
|
13
|
+
def close
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
REGISTRATION = Registration.new.freeze
|
|
18
|
+
|
|
19
|
+
# Ignore signal handlers.
|
|
20
|
+
# @parameter handlers [Handlers] The handlers to ignore.
|
|
21
|
+
# @returns [Registration] The no-op registration.
|
|
22
|
+
def self.install(handlers)
|
|
23
|
+
if block_given?
|
|
24
|
+
yield handlers
|
|
25
|
+
else
|
|
26
|
+
REGISTRATION
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/async/signals.rb
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "signals/version"
|
|
7
|
+
require_relative "signals/context"
|
|
7
8
|
require_relative "signals/handlers"
|
|
8
9
|
require_relative "signals/controller"
|
|
10
|
+
require_relative "signals/ignore"
|
|
9
11
|
|
|
10
12
|
module Async
|
|
11
13
|
# Provides composable process signal handling.
|
|
@@ -18,6 +20,16 @@ module Async
|
|
|
18
20
|
CONTROLLER
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# The default signal backend for the current thread.
|
|
24
|
+
# @returns [Async::Signals | Async::Signals::Ignore] The default signal backend.
|
|
25
|
+
def self.default
|
|
26
|
+
if ::Thread.current == ::Thread.main
|
|
27
|
+
self
|
|
28
|
+
else
|
|
29
|
+
Ignore
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
21
33
|
# Install signal handlers using the process-wide signal controller.
|
|
22
34
|
# @parameter handlers [Handlers] The handlers to install.
|
|
23
35
|
# @returns [Controller::Registration] The active registration.
|
data/readme.md
CHANGED
|
@@ -9,6 +9,7 @@ Composable process signal handling for Ruby.
|
|
|
9
9
|
- Coordinates process-wide signal traps across multiple consumers.
|
|
10
10
|
- Supports overlapping signal handlers without replacing each other.
|
|
11
11
|
- Supports scoped ignore handlers for specific signals.
|
|
12
|
+
- Provides a no-op signal backend for components that should not install process signal traps.
|
|
12
13
|
- Restores previous signal traps when handlers are removed.
|
|
13
14
|
- Resets inherited signal state in forked children on Ruby implementations with `Process._fork`.
|
|
14
15
|
- Documents thread-safe signal handler design for portable signal delivery.
|
|
@@ -23,6 +24,14 @@ Please see the [project documentation](https://socketry.github.io/async-signals/
|
|
|
23
24
|
|
|
24
25
|
Please see the [project releases](https://socketry.github.io/async-signals/releases/index) for all releases.
|
|
25
26
|
|
|
27
|
+
### v0.3.0
|
|
28
|
+
|
|
29
|
+
- Pass the installing context as the second signal handler argument and allow handler exceptions to propagate.
|
|
30
|
+
|
|
31
|
+
### v0.2.0
|
|
32
|
+
|
|
33
|
+
- Add `Async::Signals.default` and `Async::Signals::Ignore` for selecting process signal handling based on the current thread.
|
|
34
|
+
|
|
26
35
|
### v0.1.0
|
|
27
36
|
|
|
28
37
|
- Initial release.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.3.0
|
|
4
|
+
|
|
5
|
+
- Pass the installing context as the second signal handler argument and allow handler exceptions to propagate.
|
|
6
|
+
|
|
7
|
+
## v0.2.0
|
|
8
|
+
|
|
9
|
+
- Add `Async::Signals.default` and `Async::Signals::Ignore` for selecting process signal handling based on the current thread.
|
|
10
|
+
|
|
3
11
|
## v0.1.0
|
|
4
12
|
|
|
5
13
|
- Initial release.
|
data.tar.gz.sig
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
k�obI��E�F7yB���BF+C�:����,�ƥ�:k�ۻc�W�ry��QQRU�u� ��:�d��� 9w}q��2���6�@zy�.�<��?c�,n��H�`N6۹�/�m�7Y�B���z�SK5�$��T�~e��)-��-Kf����_%�O��<��L�u�K�1�;ޠS6:��-
|
|
1
|
+
,�7Hk22�"�9������;-U�`X�D*ց;7�O�ї_��C孛���1��<E���v�4����C$�b"i����c�Çy�P�@������<���K�Q?\8����O��U:��)|S�;�(A� �
|
|
2
|
+
�nn������J�f�Ԁ�~!��X�ٜP�(��8b�D�$��B�vwz��fs�-G/E�K*~�X��&_���
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-signals
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -47,8 +47,10 @@ files:
|
|
|
47
47
|
- guides/getting-started/readme.md
|
|
48
48
|
- guides/links.yaml
|
|
49
49
|
- lib/async/signals.rb
|
|
50
|
+
- lib/async/signals/context.rb
|
|
50
51
|
- lib/async/signals/controller.rb
|
|
51
52
|
- lib/async/signals/handlers.rb
|
|
53
|
+
- lib/async/signals/ignore.rb
|
|
52
54
|
- lib/async/signals/version.rb
|
|
53
55
|
- license.md
|
|
54
56
|
- readme.md
|
metadata.gz.sig
CHANGED
|
Binary file
|