async-container 0.24.0 → 0.26.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/lib/async/container/error.rb +10 -0
- data/lib/async/container/forked.rb +12 -6
- data/lib/async/container/generic.rb +1 -1
- data/lib/async/container/group.rb +69 -25
- data/lib/async/container/hybrid.rb +1 -1
- data/lib/async/container/keyed.rb +1 -1
- data/lib/async/container/notify/client.rb +1 -1
- data/lib/async/container/notify/log.rb +9 -2
- data/lib/async/container/notify/pipe.rb +4 -4
- data/lib/async/container/notify/server.rb +1 -1
- data/lib/async/container/notify/socket.rb +1 -1
- data/lib/async/container/notify.rb +1 -1
- data/lib/async/container/statistics.rb +1 -1
- data/lib/async/container/threaded.rb +12 -2
- data/lib/async/container/version.rb +2 -2
- data/lib/async/container.rb +1 -1
- data/readme.md +16 -0
- data/releases.md +20 -0
- data.tar.gz.sig +0 -0
- metadata +4 -4
- 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: 3b5ef8adc5ee828c6c044454180bf3a0de60eca666c4fa00a9c3f6b65c3ddff6
|
4
|
+
data.tar.gz: 778afef1c04f76a74dd03feecc0af119890875bb3dcfcfb4526556615ee789aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0736e1e1e2cdbed60648aac53ecf2ab258436f08f44427cc1cbc516a24a584cc9cd82449129faaf6d71a1486a3a781240de2725f6fe57a0d369ec4b6e46df4c3
|
7
|
+
data.tar.gz: 330e8827656fdae8a1943768519a9642f8dccd598e7c036b7b22ecf0043757ddccbb2b5fd1592c26b6112ce43e4aa29541876fe494beca186cadde096e071381
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -21,6 +21,16 @@ module Async
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
# Similar to {Terminate}, but represents `SIGKILL`.
|
25
|
+
class Kill < SignalException
|
26
|
+
SIGKILL = Signal.list["KILL"]
|
27
|
+
|
28
|
+
# Create a new kill error.
|
29
|
+
def initialize
|
30
|
+
super(SIGKILL)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
24
34
|
# Similar to {Interrupt}, but represents `SIGHUP`.
|
25
35
|
class Restart < SignalException
|
26
36
|
SIGHUP = Signal.list["HUP"]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "error"
|
7
7
|
|
@@ -189,6 +189,7 @@ module Async
|
|
189
189
|
"\#<#{self.class} name=#{@name.inspect} status=#{@status.inspect} pid=#{@pid.inspect}>"
|
190
190
|
end
|
191
191
|
|
192
|
+
# @returns [String] A string representation of the process.
|
192
193
|
alias to_s inspect
|
193
194
|
|
194
195
|
# Invoke {#terminate!} and then {#wait} for the child process to exit.
|
@@ -230,20 +231,25 @@ module Async
|
|
230
231
|
# Wait for the child process to exit.
|
231
232
|
# @asynchronous This method may block.
|
232
233
|
#
|
234
|
+
# @parameter timeout [Numeric | Nil] Maximum time to wait before forceful termination.
|
233
235
|
# @returns [::Process::Status] The process exit status.
|
234
|
-
def wait
|
236
|
+
def wait(timeout = 0.1)
|
235
237
|
if @pid && @status.nil?
|
236
238
|
Console.debug(self, "Waiting for process to exit...", pid: @pid)
|
237
239
|
|
238
240
|
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
|
239
|
-
|
240
|
-
|
241
|
-
sleep(
|
241
|
+
|
242
|
+
if @status.nil?
|
243
|
+
sleep(timeout) if timeout
|
242
244
|
|
243
245
|
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
|
244
246
|
|
245
247
|
if @status.nil?
|
246
|
-
Console.warn(self) {"Process #{@pid} is blocking,
|
248
|
+
Console.warn(self) {"Process #{@pid} is blocking, sending kill signal..."}
|
249
|
+
self.kill!
|
250
|
+
|
251
|
+
# Wait for the process to exit:
|
252
|
+
_, @status = ::Process.wait2(@pid)
|
247
253
|
end
|
248
254
|
end
|
249
255
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "fiber"
|
7
7
|
require "async/clock"
|
@@ -119,36 +119,78 @@ module Async
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
#
|
123
|
-
#
|
124
|
-
def
|
125
|
-
Console.
|
126
|
-
|
127
|
-
|
122
|
+
# Kill all running processes.
|
123
|
+
# This resumes the controlling fiber with an instance of {Kill}.
|
124
|
+
def kill
|
125
|
+
Console.info(self, "Sending kill to #{@running.size} running processes...")
|
126
|
+
@running.each_value do |fiber|
|
127
|
+
fiber.resume(Kill)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private def wait_for_exit(clock, timeout)
|
132
|
+
while self.any?
|
133
|
+
duration = timeout - clock.total
|
134
|
+
|
135
|
+
if duration >= 0
|
136
|
+
self.wait_for_children(duration)
|
137
|
+
else
|
138
|
+
self.wait_for_children(0)
|
139
|
+
break
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Stop all child processes with a multi-phase shutdown sequence.
|
145
|
+
#
|
146
|
+
# A graceful shutdown performs the following sequence:
|
147
|
+
# 1. Send SIGINT and wait up to `interrupt_timeout` seconds
|
148
|
+
# 2. Send SIGTERM and wait up to `terminate_timeout` seconds
|
149
|
+
# 3. Send SIGKILL and wait indefinitely for process cleanup
|
150
|
+
#
|
151
|
+
# If `graceful` is false, skips the SIGINT phase and goes directly to SIGTERM → SIGKILL.
|
152
|
+
#
|
153
|
+
# @parameter graceful [Boolean] Whether to send SIGINT first or skip directly to SIGTERM.
|
154
|
+
# @parameter interrupt_timeout [Numeric | Nil] Time to wait after SIGINT before escalating to SIGTERM.
|
155
|
+
# @parameter terminate_timeout [Numeric | Nil] Time to wait after SIGTERM before escalating to SIGKILL.
|
156
|
+
def stop(graceful = true, interrupt_timeout: 1, terminate_timeout: 1)
|
157
|
+
case graceful
|
158
|
+
when true
|
159
|
+
# Use defaults.
|
160
|
+
when false
|
161
|
+
interrupt_timeout = nil
|
162
|
+
when Numeric
|
163
|
+
interrupt_timeout = graceful
|
164
|
+
terminate_timeout = graceful
|
165
|
+
end
|
128
166
|
|
129
|
-
|
130
|
-
|
167
|
+
Console.debug(self, "Stopping all processes...", interrupt_timeout: interrupt_timeout, terminate_timeout: terminate_timeout)
|
168
|
+
|
169
|
+
# If a timeout is specified, interrupt the children first:
|
170
|
+
if interrupt_timeout
|
171
|
+
clock = Async::Clock.start
|
131
172
|
|
173
|
+
# Interrupt the children:
|
132
174
|
self.interrupt
|
133
175
|
|
134
|
-
|
135
|
-
|
136
|
-
remaining = timeout - duration
|
137
|
-
|
138
|
-
if remaining >= 0
|
139
|
-
self.wait_for_children(duration)
|
140
|
-
else
|
141
|
-
self.wait_for_children(0)
|
142
|
-
break
|
143
|
-
end
|
144
|
-
end
|
176
|
+
# Wait for the children to exit:
|
177
|
+
self.wait_for_exit(clock, interrupt_timeout)
|
145
178
|
end
|
146
179
|
|
147
|
-
|
148
|
-
|
180
|
+
if terminate_timeout
|
181
|
+
clock = Async::Clock.start
|
182
|
+
|
183
|
+
# If the children are still running, terminate them:
|
184
|
+
self.terminate
|
185
|
+
|
186
|
+
# Wait for the children to exit:
|
187
|
+
self.wait_for_exit(clock, terminate_timeout)
|
188
|
+
end
|
149
189
|
|
150
|
-
|
151
|
-
|
190
|
+
if any?
|
191
|
+
self.kill
|
192
|
+
self.wait
|
193
|
+
end
|
152
194
|
end
|
153
195
|
|
154
196
|
# Wait for a message in the specified {Channel}.
|
@@ -165,6 +207,8 @@ module Async
|
|
165
207
|
channel.interrupt!
|
166
208
|
elsif result == Terminate
|
167
209
|
channel.terminate!
|
210
|
+
elsif result == Kill
|
211
|
+
channel.kill!
|
168
212
|
elsif result
|
169
213
|
yield result
|
170
214
|
elsif message = channel.receive
|
@@ -184,7 +228,7 @@ module Async
|
|
184
228
|
# This log is a big noisy and doesn't really provide a lot of useful information.
|
185
229
|
# Console.debug(self, "Waiting for children...", duration: duration, running: @running)
|
186
230
|
|
187
|
-
|
231
|
+
unless @running.empty?
|
188
232
|
# Maybe consider using a proper event loop here:
|
189
233
|
if ready = self.select(duration)
|
190
234
|
ready.each do |io|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright,
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "client"
|
7
7
|
require "socket"
|
@@ -14,9 +14,16 @@ module Async
|
|
14
14
|
# The name of the environment variable which contains the path to the notification socket.
|
15
15
|
NOTIFY_LOG = "NOTIFY_LOG"
|
16
16
|
|
17
|
+
# @returns [String] The path to the notification log file.
|
18
|
+
# @parameter environment [Hash] The environment variables, defaults to `ENV`.
|
19
|
+
def self.path(environment = ENV)
|
20
|
+
environment[NOTIFY_LOG]
|
21
|
+
end
|
22
|
+
|
17
23
|
# Open a notification client attached to the current {NOTIFY_LOG} if possible.
|
24
|
+
# @parameter environment [Hash] The environment variables, defaults to `ENV`.
|
18
25
|
def self.open!(environment = ENV)
|
19
|
-
if path =
|
26
|
+
if path = self.path(environment)
|
20
27
|
self.new(path)
|
21
28
|
end
|
22
29
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2020-
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Juan Antonio Martín Lucas.
|
6
6
|
|
7
7
|
require_relative "client"
|
@@ -42,18 +42,18 @@ module Async
|
|
42
42
|
if notify_pipe = options.delete(:notify_pipe)
|
43
43
|
options[notify_pipe] = @io
|
44
44
|
environment[NOTIFY_PIPE] = notify_pipe.to_s
|
45
|
-
|
45
|
+
|
46
46
|
# Use stdout if it's not redirected:
|
47
47
|
# This can cause issues if the user expects stdout to be connected to a terminal.
|
48
48
|
# elsif !options.key?(:out)
|
49
49
|
# options[:out] = @io
|
50
50
|
# environment[NOTIFY_PIPE] = "1"
|
51
|
-
|
51
|
+
|
52
52
|
# Use fileno 3 if it's available:
|
53
53
|
elsif !options.key?(3)
|
54
54
|
options[3] = @io
|
55
55
|
environment[NOTIFY_PIPE] = "3"
|
56
|
-
|
56
|
+
|
57
57
|
# Otherwise, give up!
|
58
58
|
else
|
59
59
|
raise ArgumentError, "Please specify valid file descriptor for notify_pipe!"
|
@@ -216,10 +216,20 @@ module Async
|
|
216
216
|
end
|
217
217
|
|
218
218
|
# Wait for the thread to exit and return he exit status.
|
219
|
+
# @asynchronous This method may block.
|
220
|
+
#
|
221
|
+
# @parameter timeout [Numeric | Nil] Maximum time to wait before forceful termination.
|
219
222
|
# @returns [Status]
|
220
|
-
def wait
|
223
|
+
def wait(timeout = 0.1)
|
221
224
|
if @waiter
|
222
|
-
|
225
|
+
Console.debug(self, "Waiting for thread to exit...", timeout: timeout)
|
226
|
+
|
227
|
+
unless @waiter.join(timeout)
|
228
|
+
Console.warn(self) {"Thread #{@thread} is blocking, sending kill signal..."}
|
229
|
+
self.kill!
|
230
|
+
@waiter.join
|
231
|
+
end
|
232
|
+
|
223
233
|
@waiter = nil
|
224
234
|
end
|
225
235
|
|
data/lib/async/container.rb
CHANGED
data/readme.md
CHANGED
@@ -18,10 +18,26 @@ Please see the [project documentation](https://socketry.github.io/async-containe
|
|
18
18
|
|
19
19
|
- [Getting Started](https://socketry.github.io/async-container/guides/getting-started/index) - This guide explains how to use `async-container` to build basic scalable systems.
|
20
20
|
|
21
|
+
- [Systemd Integration](https://socketry.github.io/async-container/guides/systemd-integration/index) - This guide explains how to use `async-container` with systemd to manage your application as a service.
|
22
|
+
|
23
|
+
- [Kubernetes Integration](https://socketry.github.io/async-container/guides/kubernetes-integration/index) - This guide explains how to use `async-container` with Kubernetes to manage your application as a containerized service.
|
24
|
+
|
21
25
|
## Releases
|
22
26
|
|
23
27
|
Please see the [project releases](https://socketry.github.io/async-container/releases/index) for all releases.
|
24
28
|
|
29
|
+
### v0.26.0
|
30
|
+
|
31
|
+
- [Production Reliability Improvements](https://socketry.github.io/async-container/releases/index#production-reliability-improvements)
|
32
|
+
|
33
|
+
### v0.25.0
|
34
|
+
|
35
|
+
- Introduce `async:container:notify:log:ready?` task for detecting process readiness.
|
36
|
+
|
37
|
+
### v0.24.0
|
38
|
+
|
39
|
+
- Add support for health check failure metrics.
|
40
|
+
|
25
41
|
### v0.23.0
|
26
42
|
|
27
43
|
- [Add support for `NOTIFY_LOG` for Kubernetes readiness probes.](https://socketry.github.io/async-container/releases/index#add-support-for-notify_log-for-kubernetes-readiness-probes.)
|
data/releases.md
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.26.0
|
4
|
+
|
5
|
+
### Production Reliability Improvements
|
6
|
+
|
7
|
+
This release significantly improves container reliability by eliminating production hangs caused by unresponsive child processes.
|
8
|
+
|
9
|
+
**SIGKILL Fallback Support**: Containers now automatically escalate to SIGKILL when child processes ignore SIGINT and SIGTERM signals. This prevents the critical production issue where containers would hang indefinitely waiting for uncooperative processes to exit.
|
10
|
+
|
11
|
+
**Hang Prevention**: Individual child processes now have timeout-based hang prevention. If a process closes its notification pipe but doesn't actually exit, the container will detect this and escalate to SIGKILL after a reasonable timeout instead of hanging forever.
|
12
|
+
|
13
|
+
**Improved Three-Phase Shutdown**: The `Group#stop()` method now uses a cleaner interrupt → terminate → kill escalation sequence with configurable timeouts for each phase, giving well-behaved processes multiple opportunities to shut down gracefully while ensuring unresponsive processes are eventually terminated.
|
14
|
+
|
15
|
+
## v0.25.0
|
16
|
+
|
17
|
+
- Introduce `async:container:notify:log:ready?` task for detecting process readiness.
|
18
|
+
|
19
|
+
## v0.24.0
|
20
|
+
|
21
|
+
- Add support for health check failure metrics.
|
22
|
+
|
3
23
|
## v0.23.0
|
4
24
|
|
5
25
|
### Add support for `NOTIFY_LOG` for Kubernetes readiness probes.
|
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.
|
4
|
+
version: 0.26.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -40,7 +40,7 @@ cert_chain:
|
|
40
40
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
41
41
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
42
42
|
-----END CERTIFICATE-----
|
43
|
-
date:
|
43
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
44
44
|
dependencies:
|
45
45
|
- !ruby/object:Gem::Dependency
|
46
46
|
name: async
|
@@ -98,14 +98,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
98
|
requirements:
|
99
99
|
- - ">="
|
100
100
|
- !ruby/object:Gem::Version
|
101
|
-
version: '3.
|
101
|
+
version: '3.2'
|
102
102
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
103
|
requirements:
|
104
104
|
- - ">="
|
105
105
|
- !ruby/object:Gem::Version
|
106
106
|
version: '0'
|
107
107
|
requirements: []
|
108
|
-
rubygems_version: 3.6.
|
108
|
+
rubygems_version: 3.6.9
|
109
109
|
specification_version: 4
|
110
110
|
summary: Abstract container-based parallelism using threads and processes where appropriate.
|
111
111
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|