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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da817e6b406c01f5ac110ec773c2500ffe01bf6f1715163583a702291512dffc
4
- data.tar.gz: 0f0b1eb81532e10ae0b06871b1607458716c021ebc180a100707db1127c6e4d3
3
+ metadata.gz: 892b6d001f6a23c4190ecb71cd652d63aea1a7331932a68f3b0f057f8cdf762e
4
+ data.tar.gz: fd8a403c5abfe0a943ce0ec4c684ea5b6ba390f6c76006ed6772d0e12532d903
5
5
  SHA512:
6
- metadata.gz: a158082bb2d16860eb6ade8b1ff4dfbb5270feda5b38300b027563de348244af545f7b2e0e547ed134d3ebb0b0dedd14adec3be9477148e6bbfc8bea5ffef2c5
7
- data.tar.gz: d22714bf8e6421f7f762c4819fe3fc73c7ac3f702e8ff77c9ee13209341b9fb26448c01149bf4aa88253695a9c612a106afccb0cf3b5b19c01f47720292870d6
6
+ metadata.gz: cc9458635104d6f45cdcde03512d1e5e3dcaf09f21fa345bad891f678e2a5f6d80177c5dd7f84bc11e0a2d2b4b0c4582bba5cea9871bae6aafefaf320b05b124
7
+ data.tar.gz: f885189f0ae71ef8c529bf17ae23c9ad080c22641716fd30e4c54c11bc0659786c030536b6448472ac9fb66667ed0736e180e0e746bfe3972cd9ad26589a8022
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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.values.freeze
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
- begin
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
@@ -7,6 +7,6 @@
7
7
  module Async
8
8
  # @namespace
9
9
  module Signals
10
- VERSION = "0.1.0"
10
+ VERSION = "0.3.0"
11
11
  end
12
12
  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
- 5
2
- ���}��yBYj����Ur��`�^���x3�+f*����qm���&�KN�n�����Şap�>UfPա��s��9�T1f �+n۵rz3Î��Ԇ��da}a[N�o�s�3��D/w¨F~2��"��>͎8���c�3�h�����d��
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�$��Bvwz��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.1.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