async-container 0.29.1 → 0.30.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: 3d92a0a4336ea40b938ef0b8b54c7dd695f8814df486fd7d80064dbb6a131722
4
- data.tar.gz: f944972f872903ce0552b72e506888f84ad9dde40c980cbd4893f59019604eef
3
+ metadata.gz: 84ca0883f2ae0742253c1f2bdb08c98152770c41609ed179a81f2249d9cc46f7
4
+ data.tar.gz: 5d551d20be155c60557783ecf4064d0d90a3748ba18d9c66324a515208671bf0
5
5
  SHA512:
6
- metadata.gz: 8ae6df190c3ea6e654604bc545f07e1b76af03c3ff02dd236997a9c09035c482b060244ba349d831f74138338d72be307b214c292915185035b38de173eb6b98
7
- data.tar.gz: 4f7a6913143f38ec54a9f727f3bf1c738d2f7f79bd777a90fd4302689ae0c67c15faf528f1a171387a35a07cd9e168101d928ffe062749d5f0ab71e6b0e1a05f
6
+ metadata.gz: fddd4b0f2ea6f459a0ad39a3e806b776abda60cb97fbface314f6f93b0690fac817b0f8b09a486aa387c1dd06dc6f6ae3c9620d146ef4e2f50318532d36ac20d
7
+ data.tar.gz: cd14547b2e6be8a9139bcadfbf5593e777d04d6f26f02cff4c808d9b2ba3c205e36acfc19af646dca95e2f1bd52edf9020d79461a6c5987304c28eb5bc271516
checksums.yaml.gz.sig CHANGED
Binary file
@@ -134,9 +134,6 @@ module Async
134
134
  if container.failed?
135
135
  @notify&.error!("Container failed to start!")
136
136
 
137
- Console.info(self, "Stopping failed container...")
138
- container.stop(false)
139
-
140
137
  raise SetupError, container
141
138
  end
142
139
 
@@ -151,9 +148,14 @@ module Async
151
148
  end
152
149
 
153
150
  @notify&.ready!(size: @container.size)
151
+ rescue => error
152
+ raise
154
153
  ensure
155
- # If we are leaving this function with an exception, try to kill the container:
156
- container&.stop(false)
154
+ # If we are leaving this function with an exception, kill the container:
155
+ if container
156
+ Console.warn(self, "Stopping failed container...", exception: error)
157
+ container.stop(false)
158
+ end
157
159
  end
158
160
 
159
161
  # Reload the existing container. Children instances will be reloaded using `SIGHUP`.
@@ -222,9 +224,10 @@ module Async
222
224
  ::Thread.current.raise(Interrupt)
223
225
  end
224
226
 
227
+ # SIGTERM behaves the same as SIGINT by default.
225
228
  terminate_action = Signal.trap(:TERM) do
226
- # $stderr.puts "Received TERM signal, terminating...", caller
227
- ::Thread.current.raise(Terminate)
229
+ # $stderr.puts "Received TERM signal, interrupting...", caller
230
+ ::Thread.current.raise(Interrupt) # Same as SIGINT
228
231
  end
229
232
 
230
233
  hangup_action = Signal.trap(:HUP) do
@@ -102,7 +102,7 @@ module Async
102
102
  ::Process.fork do
103
103
  # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
104
104
  Signal.trap(:INT){::Thread.current.raise(Interrupt)}
105
- Signal.trap(:TERM){::Thread.current.raise(Terminate)}
105
+ Signal.trap(:TERM){::Thread.current.raise(Interrupt)} # Same as SIGINT.
106
106
  Signal.trap(:HUP){::Thread.current.raise(Restart)}
107
107
 
108
108
  # This could be a configuration option:
@@ -245,7 +245,7 @@ module Async
245
245
  _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
246
246
 
247
247
  if @status.nil?
248
- Console.warn(self, "Process is blocking, sending kill signal...", child: {process_id: @pid}, caller: caller_locations, timeout: timeout)
248
+ Console.warn(self, "Process is blocking, sending kill signal...", child: {process_id: @pid}, timeout: timeout)
249
249
  self.kill!
250
250
 
251
251
  # Wait for the process to exit:
@@ -126,7 +126,7 @@ module Async
126
126
  self.sleep
127
127
 
128
128
  if self.status?(:ready)
129
- Console.logger.debug(self) do |buffer|
129
+ Console.debug(self) do |buffer|
130
130
  buffer.puts "All ready:"
131
131
  @state.each do |child, state|
132
132
  buffer.puts "\t#{child.inspect}: #{state}"
@@ -141,7 +141,7 @@ module Async
141
141
  # Stop the children instances.
142
142
  # @parameter timeout [Boolean | Numeric] Whether to stop gracefully, or a specific timeout.
143
143
  def stop(timeout = true)
144
- Console.debug(self, "Stopping container...", timeout: timeout)
144
+ Console.info(self, "Stopping container...", timeout: timeout)
145
145
  @running = false
146
146
  @group.stop(timeout)
147
147
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
4
+ # Copyright, 2018-2026, by Samuel Williams.
5
5
 
6
6
  require "fiber"
7
7
  require "async/clock"
@@ -10,11 +10,20 @@ require_relative "error"
10
10
 
11
11
  module Async
12
12
  module Container
13
- # The default timeout for interrupting processes, before escalating to terminating.
14
- INTERRUPT_TIMEOUT = ENV.fetch("ASYNC_CONTAINER_INTERRUPT_TIMEOUT", 10).to_f
15
-
16
13
  # The default timeout for terminating processes, before escalating to killing.
17
- TERMINATE_TIMEOUT = ENV.fetch("ASYNC_CONTAINER_TERMINATE_TIMEOUT", 10).to_f
14
+ GRACEFUL_TIMEOUT = ENV.fetch("ASYNC_CONTAINER_GRACEFUL_TIMEOUT", "true").then do |value|
15
+ case value
16
+ when "true"
17
+ true # Default timeout for graceful termination.
18
+ when "false"
19
+ false # Immediately kill the processes.
20
+ else
21
+ value.to_f
22
+ end
23
+ end
24
+
25
+ # The default timeout for graceful termination.
26
+ DEFAULT_GRACEFUL_TIMEOUT = 10.0
18
27
 
19
28
  # Manages a group of running processes.
20
29
  class Group
@@ -155,50 +164,37 @@ module Async
155
164
  # Stop all child processes with a multi-phase shutdown sequence.
156
165
  #
157
166
  # A graceful shutdown performs the following sequence:
158
- # 1. Send SIGINT and wait up to `interrupt_timeout` seconds
159
- # 2. Send SIGTERM and wait up to `terminate_timeout` seconds
160
- # 3. Send SIGKILL and wait indefinitely for process cleanup
167
+ # 1. Send SIGINT and wait up to `graceful` seconds if specified.
168
+ # 2. Send SIGKILL and wait indefinitely for process cleanup.
161
169
  #
162
- # If `graceful` is false, skips the SIGINT phase and goes directly to SIGTERM SIGKILL.
170
+ # If `graceful` is true, default to `DEFAULT_GRACEFUL_TIMEOUT` (10 seconds).
171
+ # If `graceful` is false, skip the SIGINT phase and go directly to SIGKILL.
163
172
  #
164
- # @parameter graceful [Boolean] Whether to send SIGINT first or skip directly to SIGTERM.
165
- # @parameter interrupt_timeout [Numeric | Nil] Time to wait after SIGINT before escalating to SIGTERM.
166
- # @parameter terminate_timeout [Numeric | Nil] Time to wait after SIGTERM before escalating to SIGKILL.
167
- def stop(graceful = true, interrupt_timeout: INTERRUPT_TIMEOUT, terminate_timeout: TERMINATE_TIMEOUT)
168
- case graceful
169
- when true
170
- # Use defaults.
171
- when false
172
- interrupt_timeout = nil
173
- when Numeric
174
- interrupt_timeout = graceful
175
- terminate_timeout = graceful
176
- end
177
-
178
- Console.debug(self, "Stopping all processes...", interrupt_timeout: interrupt_timeout, terminate_timeout: terminate_timeout)
173
+ # @parameter graceful [Boolean | Numeric] Whether to send SIGINT first or skip directly to SIGKILL.
174
+ def stop(graceful = GRACEFUL_TIMEOUT)
175
+ Console.debug(self, "Stopping all processes...", graceful: graceful)
179
176
 
180
177
  # If a timeout is specified, interrupt the children first:
181
- if interrupt_timeout
182
- clock = Async::Clock.start
183
-
184
- # Interrupt the children:
178
+ if graceful
179
+ # Send SIGINT to the children:
185
180
  self.interrupt
186
181
 
187
- # Wait for the children to exit:
188
- self.wait_for_exit(clock, interrupt_timeout)
189
- end
190
-
191
- if terminate_timeout and self.any?
192
- clock = Async::Clock.start
182
+ if graceful == true
183
+ graceful = DEFAULT_GRACEFUL_TIMEOUT
184
+ end
193
185
 
194
- # If the children are still running, terminate them:
195
- self.terminate
186
+ clock = Clock.start
196
187
 
197
188
  # Wait for the children to exit:
198
- self.wait_for_exit(clock, terminate_timeout)
189
+ self.wait_for_exit(clock, graceful)
199
190
  end
200
-
191
+ ensure
192
+ # Do our best to clean up the children:
201
193
  if any?
194
+ if graceful
195
+ Console.warn(self, "Killing processes after graceful shutdown failed...", size: self.size, clock: clock)
196
+ end
197
+
202
198
  self.kill
203
199
  self.wait
204
200
  end
@@ -69,7 +69,7 @@ module Async
69
69
  @io.flush
70
70
  end
71
71
 
72
- private
72
+ private
73
73
 
74
74
  def environment_for(arguments)
75
75
  # Insert or duplicate the environment hash which is the first argument:
@@ -129,6 +129,15 @@ module Async
129
129
  end
130
130
  end
131
131
  end
132
+
133
+ # Wait until a "ready" message is received from the child process.
134
+ def wait_until_ready
135
+ while message = receive
136
+ if message[:ready] == true
137
+ return
138
+ end
139
+ end
140
+ end
132
141
  end
133
142
  end
134
143
  end
@@ -225,7 +225,7 @@ module Async
225
225
  Console.debug(self, "Waiting for thread to exit...", child: {thread_id: @thread.object_id}, timeout: timeout)
226
226
 
227
227
  unless @waiter.join(timeout)
228
- Console.warn(self, "Thread is blocking, sending kill signal...", child: {thread_id: @thread.object_id}, caller: caller_locations, timeout: timeout)
228
+ Console.warn(self, "Thread is blocking, sending kill signal...", child: {thread_id: @thread.object_id}, timeout: timeout)
229
229
  self.kill!
230
230
  @waiter.join
231
231
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Container
8
- VERSION = "0.29.1"
8
+ VERSION = "0.30.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -26,6 +26,11 @@ Please see the [project documentation](https://socketry.github.io/async-containe
26
26
 
27
27
  Please see the [project releases](https://socketry.github.io/async-container/releases/index) for all releases.
28
28
 
29
+ ### v0.30.0
30
+
31
+ - `SIGTERM` is now graceful, the same as `SIGINT`, for better compatibility with Kubernetes and systemd.
32
+ - `ASYNC_CONTAINER_INTERRUPT_TIMEOUT` and `ASYNC_CONTAINER_TERMINATE_TIMEOUT` are removed and replaced by `ASYNC_CONTAINER_GRACEFUL_TIMEOUT`.
33
+
29
34
  ### v0.29.0
30
35
 
31
36
  - Introduce `Client#healthy!` for sending health check messages.
@@ -65,10 +70,6 @@ Please see the [project releases](https://socketry.github.io/async-container/rel
65
70
 
66
71
  - [Production Reliability Improvements](https://socketry.github.io/async-container/releases/index#production-reliability-improvements)
67
72
 
68
- ### v0.25.0
69
-
70
- - Introduce `async:container:notify:log:ready?` task for detecting process readiness.
71
-
72
73
  ## Contributing
73
74
 
74
75
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.30.0
4
+
5
+ - `SIGTERM` is now graceful, the same as `SIGINT`, for better compatibility with Kubernetes and systemd.
6
+ - `ASYNC_CONTAINER_INTERRUPT_TIMEOUT` and `ASYNC_CONTAINER_TERMINATE_TIMEOUT` are removed and replaced by `ASYNC_CONTAINER_GRACEFUL_TIMEOUT`.
7
+
3
8
  ## v0.29.0
4
9
 
5
10
  - Introduce `Client#healthy!` for sending health check messages.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.1
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file