async-container 0.27.1 → 0.27.7

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: c475605b174dc9677a89310e85b7ca28269b084469b51c6f7f5226ca39d26340
4
- data.tar.gz: ccbdcaa3da1c785abdadefc3ece4ce6552ea6c418a0218229e04ab53c51b2637
3
+ metadata.gz: 4e0f77eb0f6eb3e7e8f8971af4d653e5ff2c3243ecc12f8dd06b27ae27e09d45
4
+ data.tar.gz: 869aaa117d285f6f407b482b5fcd5b3bb19edcba215202d2faefe159195eed38
5
5
  SHA512:
6
- metadata.gz: 1f17a77ff0978894a46b5b18b4ac7e2fc2bf451d13c2c4c4a94c122cc205df90b248007ad2b1d6d4655b89257bb94d745a57aa23ed833702f811afe62dc64261
7
- data.tar.gz: 7415b0fb06ccaeea5645f0cc43e098f8ea1f684fc3cc51b1e5dd22dd1b3be68eba864af8f32fe93d960af3e6205a490453c48189730371a19cf0f873cb738155
6
+ metadata.gz: f1eb59fda7b292a5ca70b15ea0b70af9d6f41551d9a4818f5820dbc9b078f3008d37f646e8cbcdc74428d3c359c9ec9e731b3b0b008a74922f379b700eda32e4
7
+ data.tar.gz: 301401ff8bcd736d2e41d1816110625ce038ff65b0a675d11e0ec900feb5550c73e6e754af9e5123dd12b692e4980deac1d197cbdad38e3a48213b38dd938171
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  require "json"
7
7
 
@@ -10,8 +10,9 @@ module Async
10
10
  # Provides a basic multi-thread/multi-process uni-directional communication channel.
11
11
  class Channel
12
12
  # Initialize the channel using a pipe.
13
- def initialize
13
+ def initialize(timeout: 1.0)
14
14
  @in, @out = ::IO.pipe
15
+ @in.timeout = timeout
15
16
  end
16
17
 
17
18
  # The input end of the pipe.
@@ -43,12 +44,11 @@ module Async
43
44
  # @returns [Hash]
44
45
  def receive
45
46
  if data = @in.gets
46
- begin
47
- return JSON.parse(data, symbolize_names: true)
48
- rescue
49
- return {line: data}
50
- end
47
+ return JSON.parse(data, symbolize_names: true)
51
48
  end
49
+ rescue => error
50
+ Console.error(self, "Error during channel receive!", error)
51
+ return nil
52
52
  end
53
53
  end
54
54
  end
@@ -91,11 +91,11 @@ module Async
91
91
  # Start the container unless it's already running.
92
92
  def start
93
93
  unless @container
94
- Console.info(self) {"Controller starting..."}
94
+ Console.info(self, "Controller starting...")
95
95
  self.restart
96
96
  end
97
97
 
98
- Console.info(self) {"Controller started..."}
98
+ Console.info(self, "Controller started...")
99
99
  end
100
100
 
101
101
  # Stop the container if it's running.
@@ -111,9 +111,9 @@ module Async
111
111
  if @container
112
112
  @notify&.restarting!
113
113
 
114
- Console.debug(self) {"Restarting container..."}
114
+ Console.info(self, "Restarting container...")
115
115
  else
116
- Console.debug(self) {"Starting container..."}
116
+ Console.info(self, "Starting container...")
117
117
  end
118
118
 
119
119
  container = self.create_container
@@ -127,13 +127,14 @@ module Async
127
127
  end
128
128
 
129
129
  # Wait for all child processes to enter the ready state.
130
- Console.debug(self, "Waiting for startup...")
130
+ Console.info(self, "Waiting for startup...")
131
131
  container.wait_until_ready
132
- Console.debug(self, "Finished startup.")
132
+ Console.info(self, "Finished startup.")
133
133
 
134
134
  if container.failed?
135
135
  @notify&.error!("Container failed to start!")
136
136
 
137
+ Console.info(self, "Stopping failed container...")
137
138
  container.stop(false)
138
139
 
139
140
  raise SetupError, container
@@ -145,7 +146,7 @@ module Async
145
146
  container = nil
146
147
 
147
148
  if old_container
148
- Console.debug(self, "Stopping old container...")
149
+ Console.info(self, "Stopping old container...")
149
150
  old_container&.stop(@graceful_stop)
150
151
  end
151
152
 
@@ -168,11 +169,9 @@ module Async
168
169
  end
169
170
 
170
171
  # Wait for all child processes to enter the ready state.
171
- Console.debug(self, "Waiting for startup...")
172
-
172
+ Console.info(self, "Waiting for startup...")
173
173
  @container.wait_until_ready
174
-
175
- Console.debug(self, "Finished startup.")
174
+ Console.info(self, "Finished startup.")
176
175
 
177
176
  if @container.failed?
178
177
  @notify.error!("Container failed to reload!")
@@ -136,8 +136,8 @@ module Async
136
136
 
137
137
  # Initialize the process.
138
138
  # @parameter name [String] The name to use for the child process.
139
- def initialize(name: nil)
140
- super()
139
+ def initialize(name: nil, **options)
140
+ super(**options)
141
141
 
142
142
  @name = name
143
143
  @status = nil
@@ -260,7 +260,6 @@ module Async
260
260
  end
261
261
  end
262
262
 
263
-
264
263
  # Start a named child process and execute the provided block in it.
265
264
  # @parameter name [String] The name (title) of the child process.
266
265
  # @parameter block [Proc] The block to execute in the child process.
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2025, by Samuel Williams.
5
+ # Copyright, 2025, by Marc-André Cournoyer.
5
6
 
6
7
  require "etc"
7
8
  require "async/clock"
@@ -139,12 +140,18 @@ module Async
139
140
  # Stop the children instances.
140
141
  # @parameter timeout [Boolean | Numeric] Whether to stop gracefully, or a specific timeout.
141
142
  def stop(timeout = true)
143
+ Console.info(self, "Stopping container...", timeout: timeout, caller: caller_locations)
142
144
  @running = false
143
145
  @group.stop(timeout)
144
146
 
145
147
  if @group.running?
146
- Console.warn(self) {"Group is still running after stopping it!"}
148
+ Console.warn(self, "Group is still running after stopping it!")
149
+ else
150
+ Console.info(self, "Group has stopped.")
147
151
  end
152
+ rescue => error
153
+ Console.error(self, "Error while stopping container!", exception: error)
154
+ raise
148
155
  ensure
149
156
  @running = true
150
157
  end
@@ -165,7 +172,7 @@ module Async
165
172
  name ||= UNNAMED
166
173
 
167
174
  if mark?(key)
168
- Console.debug(self) {"Reusing existing child for #{key}: #{name}"}
175
+ Console.debug(self, "Reusing existing child.", child: {key: key, name: name})
169
176
  return false
170
177
  end
171
178
 
@@ -173,15 +180,20 @@ module Async
173
180
 
174
181
  fiber do
175
182
  while @running
176
- child = self.start(name, &block)
183
+ Console.debug(self, "Starting child...", child: {key: key, name: name, restart: restart, health_check_timeout: health_check_timeout}, statistics: @statistics)
177
184
 
185
+ child = self.start(name, &block)
178
186
  state = insert(key, child)
179
187
 
188
+ Console.debug(self, "Started child.", child: child, spawn: {key: key, restart: restart, health_check_timeout: health_check_timeout}, statistics: @statistics)
189
+
180
190
  # 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.
181
191
  if health_check_timeout
182
192
  age_clock = state[:age] = Clock.start
183
193
  end
184
194
 
195
+ status = nil
196
+
185
197
  begin
186
198
  status = @group.wait_for(child) do |message|
187
199
  case message
@@ -194,15 +206,17 @@ module Async
194
206
  age_clock&.reset!
195
207
  end
196
208
  end
209
+ rescue => error
210
+ Console.error(self, "Error during child process management!", exception: error, running: @running)
197
211
  ensure
198
212
  delete(key, child)
199
213
  end
200
214
 
201
- if status.success?
202
- Console.debug(self) {"#{child} exited with #{status}"}
215
+ if status&.success?
216
+ Console.debug(self, "Child exited successfully.", status: status, running: @running)
203
217
  else
204
218
  @statistics.failure!
205
- Console.error(self, status: status)
219
+ Console.error(self, "Child exited with error!", status: status, running: @running)
206
220
  end
207
221
 
208
222
  if restart
@@ -100,9 +100,14 @@ module Async
100
100
  end
101
101
  end
102
102
 
103
+ private def each_running(&block)
104
+ # We create a copy of the values here, in case the block modifies the running set:
105
+ @running.values.each(&block)
106
+ end
107
+
103
108
  # Perform a health check on all running processes.
104
109
  def health_check!
105
- @running.each_value do |fiber|
110
+ each_running do |fiber|
106
111
  fiber.resume(:health_check!)
107
112
  end
108
113
  end
@@ -111,7 +116,7 @@ module Async
111
116
  # This resumes the controlling fiber with an instance of {Interrupt}.
112
117
  def interrupt
113
118
  Console.info(self, "Sending interrupt to #{@running.size} running processes...")
114
- @running.each_value do |fiber|
119
+ each_running do |fiber|
115
120
  fiber.resume(Interrupt)
116
121
  end
117
122
  end
@@ -120,7 +125,7 @@ module Async
120
125
  # This resumes the controlling fiber with an instance of {Terminate}.
121
126
  def terminate
122
127
  Console.info(self, "Sending terminate to #{@running.size} running processes...")
123
- @running.each_value do |fiber|
128
+ each_running do |fiber|
124
129
  fiber.resume(Terminate)
125
130
  end
126
131
  end
@@ -129,7 +134,7 @@ module Async
129
134
  # This resumes the controlling fiber with an instance of {Kill}.
130
135
  def kill
131
136
  Console.info(self, "Sending kill to #{@running.size} running processes...")
132
- @running.each_value do |fiber|
137
+ each_running do |fiber|
133
138
  fiber.resume(Kill)
134
139
  end
135
140
  end
@@ -183,7 +188,7 @@ module Async
183
188
  self.wait_for_exit(clock, interrupt_timeout)
184
189
  end
185
190
 
186
- if terminate_timeout
191
+ if terminate_timeout and self.any?
187
192
  clock = Async::Clock.start
188
193
 
189
194
  # If the children are still running, terminate them:
@@ -231,8 +236,8 @@ module Async
231
236
  protected
232
237
 
233
238
  def wait_for_children(duration = nil)
234
- # This log is a big noisy and doesn't really provide a lot of useful information.
235
- # Console.debug(self, "Waiting for children...", duration: duration, running: @running)
239
+ # This log is a bit noisy and doesn't really provide a lot of useful information:
240
+ Console.debug(self, "Waiting for children...", duration: duration, running: @running)
236
241
 
237
242
  unless @running.empty?
238
243
  # Maybe consider using a proper event loop here:
@@ -63,9 +63,9 @@ module Async
63
63
  # Formats the message using JSON and sends it to the parent controller.
64
64
  # This is suitable for use with {Channel}.
65
65
  def send(**message)
66
- data = ::JSON.dump(message)
66
+ data = ::JSON.dump(message) << "\n"
67
67
 
68
- @io.puts(data)
68
+ @io.write(data)
69
69
  @io.flush
70
70
  end
71
71
 
@@ -56,6 +56,24 @@ module Async
56
56
  @restarts += other.restarts
57
57
  @failures += other.failures
58
58
  end
59
+
60
+ # Generate a hash representation of the statistics.
61
+ #
62
+ # @returns [Hash] The statistics as a hash.
63
+ def as_json(...)
64
+ {
65
+ spawns: @spawns,
66
+ restarts: @restarts,
67
+ failures: @failures,
68
+ }
69
+ end
70
+
71
+ # Generate a JSON representation of the statistics.
72
+ #
73
+ # @returns [String] The statistics as JSON.
74
+ def to_json(...)
75
+ as_json.to_json(...)
76
+ end
59
77
  end
60
78
  end
61
79
  end
@@ -124,8 +124,8 @@ module Async
124
124
  # Initialize the thread.
125
125
  #
126
126
  # @parameter name [String] The name to use for the child thread.
127
- def initialize(name: nil)
128
- super()
127
+ def initialize(name: nil, **options)
128
+ super(**options)
129
129
 
130
130
  @status = nil
131
131
 
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Container
8
- VERSION = "0.27.1"
8
+ VERSION = "0.27.7"
9
9
  end
10
10
  end
data/license.md CHANGED
@@ -5,6 +5,7 @@ Copyright, 2019, by Yuji Yaginuma.
5
5
  Copyright, 2020, by Olle Jonsson.
6
6
  Copyright, 2020, by Juan Antonio Martín Lucas.
7
7
  Copyright, 2022, by Anton Sozontov.
8
+ Copyright, 2025, by Marc-André Cournoyer.
8
9
 
9
10
  Permission is hereby granted, free of charge, to any person obtaining a copy
10
11
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -26,6 +26,23 @@ 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.27.5
30
+
31
+ - Make the child handling more robust in the face of exceptions.
32
+
33
+ ### v0.27.4
34
+
35
+ - Fix race condition where `wait_for` could modify `@running` while it was being iterated over (`each_value`) during health checks.
36
+
37
+ ### v0.27.3
38
+
39
+ - Add log for starting child, including container statistics.
40
+ - Don't try to (log) "terminate 0 child processes" if there are none.
41
+
42
+ ### v0.27.2
43
+
44
+ - More logging, especially around failure cases.
45
+
29
46
  ### v0.27.1
30
47
 
31
48
  - Log caller and timeout when waiting on a child instance to exit, if it blocks.
@@ -51,21 +68,6 @@ Please see the [project releases](https://socketry.github.io/async-container/rel
51
68
 
52
69
  - [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.)
53
70
 
54
- ### v0.21.0
55
-
56
- - Use `SIGKILL`/`Thread#kill` when the health check fails. In some cases, `SIGTERM` may not be sufficient to terminate a process because the signal can be ignored or the process may be in an uninterruptible state.
57
-
58
- ### v0.20.1
59
-
60
- - Fix compatibility between <code class="language-ruby">Async::Container::Hybrid</code> and the health check.
61
- - <code class="language-ruby">Async::Container::Generic\#initialize</code> passes unused arguments through to <code class="language-ruby">Async::Container::Group</code>.
62
-
63
- ### v0.20.0
64
-
65
- - Improve container signal handling reliability by using `Thread.handle_interrupt` except at known safe points.
66
- - Improved logging when child process fails and container startup.
67
- - [Add `health_check_timeout` for detecting hung processes.](https://socketry.github.io/async-container/releases/index#add-health_check_timeout-for-detecting-hung-processes.)
68
-
69
71
  ## Contributing
70
72
 
71
73
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Releases
2
2
 
3
+ ## v0.27.5
4
+
5
+ - Make the child handling more robust in the face of exceptions.
6
+
7
+ ## v0.27.4
8
+
9
+ - Fix race condition where `wait_for` could modify `@running` while it was being iterated over (`each_value`) during health checks.
10
+
11
+ ## v0.27.3
12
+
13
+ - Add log for starting child, including container statistics.
14
+ - Don't try to (log) "terminate 0 child processes" if there are none.
15
+
16
+ ## v0.27.2
17
+
18
+ - More logging, especially around failure cases.
19
+
3
20
  ## v0.27.1
4
21
 
5
22
  - Log caller and timeout when waiting on a child instance to exit, if it blocks.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.1
4
+ version: 0.27.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Olle Jonsson
9
9
  - Anton Sozontov
10
10
  - Juan Antonio Martín Lucas
11
+ - Marc-André Cournoyer
11
12
  - Yuji Yaginuma
12
13
  bindir: bin
13
14
  cert_chain:
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  - !ruby/object:Gem::Version
106
107
  version: '0'
107
108
  requirements: []
108
- rubygems_version: 3.6.9
109
+ rubygems_version: 3.7.2
109
110
  specification_version: 4
110
111
  summary: Abstract container-based parallelism using threads and processes where appropriate.
111
112
  test_files: []
metadata.gz.sig CHANGED
Binary file