async-container 0.19.0 → 0.20.1
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/forked.rb +18 -0
- data/lib/async/container/generic.rb +22 -3
- data/lib/async/container/group.rb +40 -3
- data/lib/async/container/hybrid.rb +4 -2
- data/lib/async/container/threaded.rb +29 -1
- data/lib/async/container/version.rb +1 -1
- data/readme.md +18 -1
- data/releases.md +42 -1
- 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: 2435dd48259ab4b6bbad01780c21dc91bdf2946485f1dd682c8388b9ee68410d
|
4
|
+
data.tar.gz: c8e3564e5c889f469658eb044444bc6884575090ed38e73f07bcffdfe893f823
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa56a0207d1bab96685c25ca8f3617a5e5aefeb673998d836780baf75e1ff9ee9618ac4e506f893de206f425277a7abb0dc12f748850d59e037f5f155763b9d9
|
7
|
+
data.tar.gz: 1198eaa52f0ea7dfe3b411c1e6cf6866f720f9aa4b5b1913fee2fbb535144115b4e2e3e21e313fa01faa64e724e7e786e32732c0889d2c8601c8ccce72dfe967
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -116,6 +116,24 @@ module Async
|
|
116
116
|
self.close_write
|
117
117
|
end
|
118
118
|
|
119
|
+
# Convert the child process to a hash, suitable for serialization.
|
120
|
+
#
|
121
|
+
# @returns [Hash] The request as a hash.
|
122
|
+
def as_json(...)
|
123
|
+
{
|
124
|
+
name: @name,
|
125
|
+
pid: @pid,
|
126
|
+
status: @status&.to_i,
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Convert the request to JSON.
|
131
|
+
#
|
132
|
+
# @returns [String] The request as JSON.
|
133
|
+
def to_json(...)
|
134
|
+
as_json.to_json(...)
|
135
|
+
end
|
136
|
+
|
119
137
|
# Set the name of the process.
|
120
138
|
# Invokes {::Process.setproctitle} if invoked in the child process.
|
121
139
|
def name= value
|
@@ -4,6 +4,7 @@
|
|
4
4
|
# Copyright, 2019-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require "etc"
|
7
|
+
require "async/clock"
|
7
8
|
|
8
9
|
require_relative "group"
|
9
10
|
require_relative "keyed"
|
@@ -38,7 +39,7 @@ module Async
|
|
38
39
|
UNNAMED = "Unnamed"
|
39
40
|
|
40
41
|
def initialize(**options)
|
41
|
-
@group = Group.new
|
42
|
+
@group = Group.new(**options)
|
42
43
|
@running = true
|
43
44
|
|
44
45
|
@state = {}
|
@@ -47,8 +48,10 @@ module Async
|
|
47
48
|
@keyed = {}
|
48
49
|
end
|
49
50
|
|
51
|
+
# @attribute [Group] The group of running children instances.
|
50
52
|
attr :group
|
51
53
|
|
54
|
+
# @attribute [Hash(Child, Hash)] The state of each child instance.
|
52
55
|
attr :state
|
53
56
|
|
54
57
|
# A human readable representation of the container.
|
@@ -141,7 +144,8 @@ module Async
|
|
141
144
|
# @parameter name [String] The name of the child instance.
|
142
145
|
# @parameter restart [Boolean] Whether to restart the child instance if it fails.
|
143
146
|
# @parameter key [Symbol] A key used for reloading child instances.
|
144
|
-
|
147
|
+
# @parameter health_check_timeout [Numeric | Nil] The maximum time a child instance can run without updating its state, before it is terminated as unhealthy.
|
148
|
+
def spawn(name: nil, restart: false, key: nil, health_check_timeout: nil, &block)
|
145
149
|
name ||= UNNAMED
|
146
150
|
|
147
151
|
if mark?(key)
|
@@ -157,9 +161,24 @@ module Async
|
|
157
161
|
|
158
162
|
state = insert(key, child)
|
159
163
|
|
164
|
+
# If a health check is specified, we will monitor the child process and terminate it if it does not update its state within the specified time.
|
165
|
+
if health_check_timeout
|
166
|
+
age_clock = state[:age] = Clock.start
|
167
|
+
end
|
168
|
+
|
160
169
|
begin
|
161
170
|
status = @group.wait_for(child) do |message|
|
162
|
-
|
171
|
+
case message
|
172
|
+
when :health_check!
|
173
|
+
if health_check_timeout&.<(age_clock.total)
|
174
|
+
Console.warn(self, "Child failed health check!", child: child, age: age_clock.total, health_check_timeout: health_check_timeout)
|
175
|
+
# If the child has failed the health check, we assume the worst and terminate it (SIGTERM).
|
176
|
+
child.terminate!
|
177
|
+
end
|
178
|
+
else
|
179
|
+
state.update(message)
|
180
|
+
age_clock&.reset!
|
181
|
+
end
|
163
182
|
end
|
164
183
|
ensure
|
165
184
|
delete(key, child)
|
@@ -13,7 +13,12 @@ module Async
|
|
13
13
|
# Manages a group of running processes.
|
14
14
|
class Group
|
15
15
|
# Initialize an empty group.
|
16
|
-
|
16
|
+
#
|
17
|
+
# @parameter health_check_interval [Numeric | Nil] The (biggest) interval at which health checks are performed.
|
18
|
+
def initialize(health_check_interval: 1.0)
|
19
|
+
@health_check_interval = health_check_interval
|
20
|
+
|
21
|
+
# The running fibers, indexed by IO:
|
17
22
|
@running = {}
|
18
23
|
|
19
24
|
# This queue allows us to wait for processes to complete, without spawning new processes as a result.
|
@@ -57,8 +62,36 @@ module Async
|
|
57
62
|
def wait
|
58
63
|
self.resume
|
59
64
|
|
60
|
-
|
61
|
-
self.wait_for_children
|
65
|
+
with_health_checks do |duration|
|
66
|
+
self.wait_for_children(duration)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private def with_health_checks
|
71
|
+
if @health_check_interval
|
72
|
+
health_check_clock = Clock.start
|
73
|
+
|
74
|
+
while self.running?
|
75
|
+
duration = [@health_check_interval - health_check_clock.total, 0].max
|
76
|
+
|
77
|
+
yield duration
|
78
|
+
|
79
|
+
if health_check_clock.total > @health_check_interval
|
80
|
+
self.health_check!
|
81
|
+
health_check_clock.reset!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
while self.running?
|
86
|
+
yield nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Perform a health check on all running processes.
|
92
|
+
def health_check!
|
93
|
+
@running.each_value do |fiber|
|
94
|
+
fiber.resume(:health_check!)
|
62
95
|
end
|
63
96
|
end
|
64
97
|
|
@@ -119,15 +152,19 @@ module Async
|
|
119
152
|
@running[io] = Fiber.current
|
120
153
|
|
121
154
|
while @running.key?(io)
|
155
|
+
# Wait for some event on the channel:
|
122
156
|
result = Fiber.yield
|
123
157
|
|
124
158
|
if result == Interrupt
|
125
159
|
channel.interrupt!
|
126
160
|
elsif result == Terminate
|
127
161
|
channel.terminate!
|
162
|
+
elsif result
|
163
|
+
yield result
|
128
164
|
elsif message = channel.receive
|
129
165
|
yield message
|
130
166
|
else
|
167
|
+
# Wait for the channel to exit:
|
131
168
|
return channel.wait
|
132
169
|
end
|
133
170
|
end
|
@@ -15,7 +15,8 @@ module Async
|
|
15
15
|
# @parameter count [Integer] The number of instances to start.
|
16
16
|
# @parameter forks [Integer] The number of processes to fork.
|
17
17
|
# @parameter threads [Integer] the number of threads to start.
|
18
|
-
|
18
|
+
# @parameter health_check_timeout [Numeric] The timeout for health checks, in seconds. Passed into the child {Threaded} containers.
|
19
|
+
def run(count: nil, forks: nil, threads: nil, health_check_timeout: nil, **options, &block)
|
19
20
|
processor_count = Container.processor_count
|
20
21
|
count ||= processor_count ** 2
|
21
22
|
forks ||= [processor_count, count].min
|
@@ -25,7 +26,7 @@ module Async
|
|
25
26
|
self.spawn(**options) do |instance|
|
26
27
|
container = Threaded.new
|
27
28
|
|
28
|
-
container.run(count: threads, **options, &block)
|
29
|
+
container.run(count: threads, health_check_timeout: health_check_timeout, **options, &block)
|
29
30
|
|
30
31
|
container.wait_until_ready
|
31
32
|
instance.ready!
|
@@ -34,6 +35,7 @@ module Async
|
|
34
35
|
rescue Async::Container::Terminate
|
35
36
|
# Stop it immediately:
|
36
37
|
container.stop(false)
|
38
|
+
raise
|
37
39
|
ensure
|
38
40
|
# Stop it gracefully (also code path for Interrupt):
|
39
41
|
container.stop
|
@@ -90,7 +90,10 @@ module Async
|
|
90
90
|
def self.fork(**options)
|
91
91
|
self.new(**options) do |thread|
|
92
92
|
::Thread.new do
|
93
|
-
|
93
|
+
# This could be a configuration option (see forked implementation too):
|
94
|
+
::Thread.handle_interrupt(SignalException => :immediate) do
|
95
|
+
yield Instance.for(thread)
|
96
|
+
end
|
94
97
|
end
|
95
98
|
end
|
96
99
|
end
|
@@ -122,6 +125,23 @@ module Async
|
|
122
125
|
end
|
123
126
|
end
|
124
127
|
|
128
|
+
# Convert the child process to a hash, suitable for serialization.
|
129
|
+
#
|
130
|
+
# @returns [Hash] The request as a hash.
|
131
|
+
def as_json(...)
|
132
|
+
{
|
133
|
+
name: @thread.name,
|
134
|
+
status: @status&.as_json,
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Convert the request to JSON.
|
139
|
+
#
|
140
|
+
# @returns [String] The request as JSON.
|
141
|
+
def to_json(...)
|
142
|
+
as_json.to_json(...)
|
143
|
+
end
|
144
|
+
|
125
145
|
# Set the name of the thread.
|
126
146
|
# @parameter value [String] The name to set.
|
127
147
|
def name= value
|
@@ -188,6 +208,14 @@ module Async
|
|
188
208
|
@error.nil?
|
189
209
|
end
|
190
210
|
|
211
|
+
def as_json(...)
|
212
|
+
if @error
|
213
|
+
@error.inspect
|
214
|
+
else
|
215
|
+
true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
191
219
|
# A human readable representation of the status.
|
192
220
|
def to_s
|
193
221
|
"\#<#{self.class} #{success? ? "success" : "failure"}>"
|
data/readme.md
CHANGED
@@ -14,7 +14,24 @@ Provides containers which implement parallelism for clients and servers.
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
-
Please see the [project documentation](https://socketry.github.io/async-container/).
|
17
|
+
Please see the [project documentation](https://socketry.github.io/async-container/) for more details.
|
18
|
+
|
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
|
+
|
21
|
+
## Releases
|
22
|
+
|
23
|
+
Please see the [project releases](https://socketry.github.io/async-container/releases/index) for all releases.
|
24
|
+
|
25
|
+
### v0.20.1
|
26
|
+
|
27
|
+
- Fix compatibility between <code class="language-ruby">Async::Container::Hybrid</code> and the health check.
|
28
|
+
- <code class="language-ruby">Async::Container::Generic\#initialize</code> passes unused arguments through to <code class="language-ruby">Async::Container::Group</code>.
|
29
|
+
|
30
|
+
### v0.20.0
|
31
|
+
|
32
|
+
- Improve container signal handling reliability by using `Thread.handle_interrupt` except at known safe points.
|
33
|
+
- Improved logging when child process fails and container startup.
|
34
|
+
- [Add `health_check_timeout` for detecting hung processes.](https://socketry.github.io/async-container/releases/index#add-health_check_timeout-for-detecting-hung-processes.)
|
18
35
|
|
19
36
|
## Contributing
|
20
37
|
|
data/releases.md
CHANGED
@@ -1,6 +1,47 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
-
##
|
3
|
+
## v0.20.1
|
4
|
+
|
5
|
+
- Fix compatibility between {ruby Async::Container::Hybrid} and the health check.
|
6
|
+
- {ruby Async::Container::Generic\#initialize} passes unused arguments through to {ruby Async::Container::Group}.
|
7
|
+
|
8
|
+
## v0.20.0
|
4
9
|
|
5
10
|
- Improve container signal handling reliability by using `Thread.handle_interrupt` except at known safe points.
|
6
11
|
- Improved logging when child process fails and container startup.
|
12
|
+
|
13
|
+
### Add `health_check_timeout` for detecting hung processes.
|
14
|
+
|
15
|
+
In order to detect hung processes, a `health_check_timeout` can be specified when spawning children workers. If the health check does not complete within the specified timeout, the child process is killed.
|
16
|
+
|
17
|
+
``` ruby
|
18
|
+
require "async/container"
|
19
|
+
|
20
|
+
container = Async::Container.new
|
21
|
+
|
22
|
+
container.run(count: 1, restart: true, health_check_timeout: 1) do |instance|
|
23
|
+
while true
|
24
|
+
# This example will fail sometimes:
|
25
|
+
sleep(0.5 + rand)
|
26
|
+
instance.ready!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
container.wait
|
31
|
+
```
|
32
|
+
|
33
|
+
If the health check does not complete within the specified timeout, the child process is killed:
|
34
|
+
|
35
|
+
```
|
36
|
+
3.01s warn: Async::Container::Forked [oid=0x1340] [ec=0x1348] [pid=27100] [2025-02-20 13:24:55 +1300]
|
37
|
+
| Child failed health check!
|
38
|
+
| {
|
39
|
+
| "child": {
|
40
|
+
| "name": "Unnamed",
|
41
|
+
| "pid": 27101,
|
42
|
+
| "status": null
|
43
|
+
| },
|
44
|
+
| "age": 1.0612829999881797,
|
45
|
+
| "health_check_timeout": 1
|
46
|
+
| }
|
47
|
+
```
|
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.20.1
|
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: 2025-02-
|
43
|
+
date: 2025-02-20 00:00:00.000000000 Z
|
44
44
|
dependencies:
|
45
45
|
- !ruby/object:Gem::Dependency
|
46
46
|
name: async
|
@@ -48,14 +48,14 @@ dependencies:
|
|
48
48
|
requirements:
|
49
49
|
- - "~>"
|
50
50
|
- !ruby/object:Gem::Version
|
51
|
-
version: '2.
|
51
|
+
version: '2.22'
|
52
52
|
type: :runtime
|
53
53
|
prerelease: false
|
54
54
|
version_requirements: !ruby/object:Gem::Requirement
|
55
55
|
requirements:
|
56
56
|
- - "~>"
|
57
57
|
- !ruby/object:Gem::Version
|
58
|
-
version: '2.
|
58
|
+
version: '2.22'
|
59
59
|
executables: []
|
60
60
|
extensions: []
|
61
61
|
extra_rdoc_files: []
|
metadata.gz.sig
CHANGED
Binary file
|