async-container 0.18.3 → 0.19.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: d061fd5261bb0fabc616df89690cddd1c7545d5fc41799bc107050da08bf7635
4
- data.tar.gz: 13a9e58f76b560a9f5602da7977739c936aa71732338f802a97d9fc9a6224480
3
+ metadata.gz: 2edeb6fa679f328736bb4a65c3b342b4aea94f2fc79557d4fc6e9e84bee77a2e
4
+ data.tar.gz: 5c1c52a4f90c8710efebb411d97e5a8342ea99d1bbe09082deed562b8acdce99
5
5
  SHA512:
6
- metadata.gz: 1f162eadf2c624ebe86fe174578096ccb473e5f9f031f75d8135ed77cf9911f006e4b3a9957b8402829a67fa480d80c7fdf3a3abdbb02e959ae1c135e99c3c92
7
- data.tar.gz: 8201e656ac4f8182380bb08af4290c51bddc62d97214fd39bf0e7761f48f339a859f809dd12539c2a53a85219e4b4b6a68bcb9a81c7ef7c9dd4bab3da563d053
6
+ metadata.gz: ee365ff16248e3064b136cdfeddec8e0f01ef77decbd514b94fc4a3bce1a38c2c51ffa5ba1e071b2fcd6bad6b7c4d4e322f26e8e5a530e4f45a68266d67429da
7
+ data.tar.gz: 171a93c5855cad3a112622232bbc7e93b880d4c77223cb8a17c7772a0687a0be40af3143a0ecad2b14a9e9aa656010cdf47eb42f80e14b08148cd02f4a4eaa6c
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require_relative 'forked'
7
- require_relative 'threaded'
8
- require_relative 'hybrid'
6
+ require_relative "forked"
7
+ require_relative "threaded"
8
+ require_relative "hybrid"
9
9
 
10
10
  module Async
11
11
  module Container
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require 'json'
6
+ require "json"
7
7
 
8
8
  module Async
9
9
  module Container
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
- require_relative 'error'
7
- require_relative 'best'
6
+ require_relative "error"
7
+ require_relative "best"
8
8
 
9
- require_relative 'statistics'
10
- require_relative 'notify'
9
+ require_relative "statistics"
10
+ require_relative "notify"
11
11
 
12
12
  module Async
13
13
  module Container
@@ -26,13 +26,10 @@ module Async
26
26
  @container = nil
27
27
  @container_class = container_class
28
28
 
29
- if @notify = notify
30
- @notify.status!("Initializing...")
31
- end
32
-
29
+ @notify = notify
33
30
  @signals = {}
34
31
 
35
- trap(SIGHUP) do
32
+ self.trap(SIGHUP) do
36
33
  self.restart
37
34
  end
38
35
 
@@ -93,7 +90,12 @@ module Async
93
90
 
94
91
  # Start the container unless it's already running.
95
92
  def start
96
- self.restart unless @container
93
+ unless @container
94
+ Console.info(self) {"Controller starting..."}
95
+ self.restart
96
+ end
97
+
98
+ Console.info(self) {"Controller started..."}
97
99
  end
98
100
 
99
101
  # Stop the container if it's running.
@@ -109,9 +111,9 @@ module Async
109
111
  if @container
110
112
  @notify&.restarting!
111
113
 
112
- Console.logger.debug(self) {"Restarting container..."}
114
+ Console.debug(self) {"Restarting container..."}
113
115
  else
114
- Console.logger.debug(self) {"Starting container..."}
116
+ Console.debug(self) {"Starting container..."}
115
117
  end
116
118
 
117
119
  container = self.create_container
@@ -125,9 +127,9 @@ module Async
125
127
  end
126
128
 
127
129
  # Wait for all child processes to enter the ready state.
128
- Console.logger.debug(self, "Waiting for startup...")
130
+ Console.debug(self, "Waiting for startup...")
129
131
  container.wait_until_ready
130
- Console.logger.debug(self, "Finished startup.")
132
+ Console.debug(self, "Finished startup.")
131
133
 
132
134
  if container.failed?
133
135
  @notify&.error!("Container failed to start!")
@@ -143,7 +145,7 @@ module Async
143
145
  container = nil
144
146
 
145
147
  if old_container
146
- Console.logger.debug(self, "Stopping old container...")
148
+ Console.debug(self, "Stopping old container...")
147
149
  old_container&.stop(@graceful_stop)
148
150
  end
149
151
 
@@ -157,7 +159,7 @@ module Async
157
159
  def reload
158
160
  @notify&.reloading!
159
161
 
160
- Console.logger.info(self) {"Reloading container: #{@container}..."}
162
+ Console.info(self) {"Reloading container: #{@container}..."}
161
163
 
162
164
  begin
163
165
  self.setup(@container)
@@ -166,11 +168,11 @@ module Async
166
168
  end
167
169
 
168
170
  # Wait for all child processes to enter the ready state.
169
- Console.logger.debug(self, "Waiting for startup...")
171
+ Console.debug(self, "Waiting for startup...")
170
172
 
171
173
  @container.wait_until_ready
172
174
 
173
- Console.logger.debug(self, "Finished startup.")
175
+ Console.debug(self, "Finished startup.")
174
176
 
175
177
  if @container.failed?
176
178
  @notify.error!("Container failed to reload!")
@@ -183,10 +185,41 @@ module Async
183
185
 
184
186
  # Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.
185
187
  def run
188
+ @notify&.status!("Initializing...")
189
+
190
+ with_signal_handlers do
191
+ self.start
192
+
193
+ while @container&.running?
194
+ begin
195
+ @container.wait
196
+ rescue SignalException => exception
197
+ if handler = @signals[exception.signo]
198
+ begin
199
+ handler.call
200
+ rescue SetupError => error
201
+ Console.error(self, error)
202
+ end
203
+ else
204
+ raise
205
+ end
206
+ end
207
+ end
208
+ end
209
+ rescue Interrupt
210
+ self.stop
211
+ rescue Terminate
212
+ self.stop(false)
213
+ ensure
214
+ self.stop(false)
215
+ end
216
+
217
+ private def with_signal_handlers
186
218
  # I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
187
- # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
219
+
188
220
  interrupt_action = Signal.trap(:INT) do
189
- # $stderr.puts "Received INT signal, terminating...", caller
221
+ # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
222
+ # $stderr.puts "Received INT signal, interrupting...", caller
190
223
  ::Thread.current.raise(Interrupt)
191
224
  end
192
225
 
@@ -197,33 +230,13 @@ module Async
197
230
 
198
231
  hangup_action = Signal.trap(:HUP) do
199
232
  # $stderr.puts "Received HUP signal, restarting...", caller
200
- ::Thread.current.raise(Hangup)
233
+ ::Thread.current.raise(Restart)
201
234
  end
202
235
 
203
- self.start
204
-
205
- while @container&.running?
206
- begin
207
- @container.wait
208
- rescue SignalException => exception
209
- if handler = @signals[exception.signo]
210
- begin
211
- handler.call
212
- rescue SetupError => error
213
- Console.logger.error(self) {error}
214
- end
215
- else
216
- raise
217
- end
218
- end
236
+ ::Thread.handle_interrupt(SignalException => :never) do
237
+ yield
219
238
  end
220
- rescue Interrupt
221
- self.stop
222
- rescue Terminate
223
- self.stop(false)
224
239
  ensure
225
- self.stop(false)
226
-
227
240
  # Restore the interrupt handler:
228
241
  Signal.trap(:INT, interrupt_action)
229
242
  Signal.trap(:TERM, terminate_action)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Container
@@ -12,15 +12,15 @@ module Async
12
12
 
13
13
  # Similar to {Interrupt}, but represents `SIGTERM`.
14
14
  class Terminate < SignalException
15
- SIGTERM = Signal.list['TERM']
15
+ SIGTERM = Signal.list["TERM"]
16
16
 
17
17
  def initialize
18
18
  super(SIGTERM)
19
19
  end
20
20
  end
21
21
 
22
- class Hangup < SignalException
23
- SIGHUP = Signal.list['HUP']
22
+ class Restart < SignalException
23
+ SIGHUP = Signal.list["HUP"]
24
24
 
25
25
  def initialize
26
26
  super(SIGHUP)
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
- require_relative 'generic'
7
- require_relative 'process'
6
+ require_relative "error"
7
+
8
+ require_relative "generic"
9
+ require_relative "channel"
10
+ require_relative "notify/pipe"
8
11
 
9
12
  module Async
10
13
  module Container
@@ -15,11 +18,190 @@ module Async
15
18
  true
16
19
  end
17
20
 
21
+ # Represents a running child process from the point of view of the parent container.
22
+ class Child < Channel
23
+ # Represents a running child process from the point of view of the child process.
24
+ class Instance < Notify::Pipe
25
+ # Wrap an instance around the {Process} instance from within the forked child.
26
+ # @parameter process [Process] The process intance to wrap.
27
+ def self.for(process)
28
+ instance = self.new(process.out)
29
+
30
+ # The child process won't be reading from the channel:
31
+ process.close_read
32
+
33
+ instance.name = process.name
34
+
35
+ return instance
36
+ end
37
+
38
+ def initialize(io)
39
+ super
40
+
41
+ @name = nil
42
+ end
43
+
44
+ # Set the process title to the specified value.
45
+ # @parameter value [String] The name of the process.
46
+ def name= value
47
+ if @name = value
48
+ ::Process.setproctitle(@name)
49
+ end
50
+ end
51
+
52
+ # The name of the process.
53
+ # @returns [String]
54
+ def name
55
+ @name
56
+ end
57
+
58
+ # Replace the current child process with a different one. Forwards arguments and options to {::Process.exec}.
59
+ # This method replaces the child process with the new executable, thus this method never returns.
60
+ def exec(*arguments, ready: true, **options)
61
+ if ready
62
+ self.ready!(status: "(exec)")
63
+ else
64
+ self.before_spawn(arguments, options)
65
+ end
66
+
67
+ ::Process.exec(*arguments, **options)
68
+ end
69
+ end
70
+
71
+ # Fork a child process appropriate for a container.
72
+ # @returns [Process]
73
+ def self.fork(**options)
74
+ # $stderr.puts fork: caller
75
+ self.new(**options) do |process|
76
+ ::Process.fork do
77
+ # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
78
+ Signal.trap(:INT) {::Thread.current.raise(Interrupt)}
79
+ Signal.trap(:TERM) {::Thread.current.raise(Terminate)}
80
+ Signal.trap(:HUP) {::Thread.current.raise(Restart)}
81
+
82
+ # This could be a configuration option:
83
+ ::Thread.handle_interrupt(SignalException => :immediate) do
84
+ yield Instance.for(process)
85
+ rescue Interrupt
86
+ # Graceful exit.
87
+ rescue Exception => error
88
+ Console.error(self, error)
89
+
90
+ exit!(1)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.spawn(*arguments, name: nil, **options)
97
+ self.new(name: name) do |process|
98
+ Notify::Pipe.new(process.out).before_spawn(arguments, options)
99
+
100
+ ::Process.spawn(*arguments, **options)
101
+ end
102
+ end
103
+
104
+ # Initialize the process.
105
+ # @parameter name [String] The name to use for the child process.
106
+ def initialize(name: nil)
107
+ super()
108
+
109
+ @name = name
110
+ @status = nil
111
+ @pid = nil
112
+
113
+ @pid = yield(self)
114
+
115
+ # The parent process won't be writing to the channel:
116
+ self.close_write
117
+ end
118
+
119
+ # Set the name of the process.
120
+ # Invokes {::Process.setproctitle} if invoked in the child process.
121
+ def name= value
122
+ @name = value
123
+
124
+ # If we are the child process:
125
+ ::Process.setproctitle(@name) if @pid.nil?
126
+ end
127
+
128
+ # The name of the process.
129
+ # @attribute [String]
130
+ attr :name
131
+
132
+ # @attribute [Integer] The process identifier.
133
+ attr :pid
134
+
135
+ # A human readable representation of the process.
136
+ # @returns [String]
137
+ def inspect
138
+ "\#<#{self.class} name=#{@name.inspect} status=#{@status.inspect} pid=#{@pid.inspect}>"
139
+ end
140
+
141
+ alias to_s inspect
142
+
143
+ # Invoke {#terminate!} and then {#wait} for the child process to exit.
144
+ def close
145
+ self.terminate!
146
+ self.wait
147
+ ensure
148
+ super
149
+ end
150
+
151
+ # Send `SIGINT` to the child process.
152
+ def interrupt!
153
+ unless @status
154
+ ::Process.kill(:INT, @pid)
155
+ end
156
+ end
157
+
158
+ # Send `SIGTERM` to the child process.
159
+ def terminate!
160
+ unless @status
161
+ ::Process.kill(:TERM, @pid)
162
+ end
163
+ end
164
+
165
+ # Send `SIGHUP` to the child process.
166
+ def restart!
167
+ unless @status
168
+ ::Process.kill(:HUP, @pid)
169
+ end
170
+ end
171
+
172
+ # Wait for the child process to exit.
173
+ # @asynchronous This method may block.
174
+ #
175
+ # @returns [::Process::Status] The process exit status.
176
+ def wait
177
+ if @pid && @status.nil?
178
+ Console.debug(self, "Waiting for process to exit...", pid: @pid)
179
+
180
+ _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
181
+
182
+ while @status.nil?
183
+ sleep(0.1)
184
+
185
+ _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
186
+
187
+ if @status.nil?
188
+ Console.warn(self) {"Process #{@pid} is blocking, has it exited?"}
189
+ end
190
+ end
191
+ end
192
+
193
+ Console.debug(self, "Process exited.", pid: @pid, status: @status)
194
+
195
+ return @status
196
+ end
197
+ end
198
+
199
+
18
200
  # Start a named child process and execute the provided block in it.
19
201
  # @parameter name [String] The name (title) of the child process.
20
202
  # @parameter block [Proc] The block to execute in the child process.
21
203
  def start(name, &block)
22
- Process.fork(name: name, &block)
204
+ Child.fork(name: name, &block)
23
205
  end
24
206
  end
25
207
  end
@@ -3,18 +3,16 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'async'
6
+ require "etc"
7
7
 
8
- require 'etc'
9
-
10
- require_relative 'group'
11
- require_relative 'keyed'
12
- require_relative 'statistics'
8
+ require_relative "group"
9
+ require_relative "keyed"
10
+ require_relative "statistics"
13
11
 
14
12
  module Async
15
13
  module Container
16
14
  # An environment variable key to override {.processor_count}.
17
- ASYNC_CONTAINER_PROCESSOR_COUNT = 'ASYNC_CONTAINER_PROCESSOR_COUNT'
15
+ ASYNC_CONTAINER_PROCESSOR_COUNT = "ASYNC_CONTAINER_PROCESSOR_COUNT"
18
16
 
19
17
  # The processor count which may be used for the default number of container threads/processes. You can override the value provided by the system by specifying the `ASYNC_CONTAINER_PROCESSOR_COUNT` environment variable.
20
18
  # @returns [Integer] The number of hardware processors which can run threads/processes simultaneously.
@@ -104,16 +102,23 @@ module Async
104
102
  # @returns [Boolean] The children all became ready.
105
103
  def wait_until_ready
106
104
  while true
107
- Console.logger.debug(self) do |buffer|
105
+ Console.debug(self) do |buffer|
108
106
  buffer.puts "Waiting for ready:"
109
107
  @state.each do |child, state|
110
- buffer.puts "\t#{child.class}: #{state.inspect}"
108
+ buffer.puts "\t#{child.inspect}: #{state}"
111
109
  end
112
110
  end
113
111
 
114
112
  self.sleep
115
113
 
116
114
  if self.status?(:ready)
115
+ Console.logger.debug(self) do |buffer|
116
+ buffer.puts "All ready:"
117
+ @state.each do |child, state|
118
+ buffer.puts "\t#{child.inspect}: #{state}"
119
+ end
120
+ end
121
+
117
122
  return true
118
123
  end
119
124
  end
@@ -126,7 +131,7 @@ module Async
126
131
  @group.stop(timeout)
127
132
 
128
133
  if @group.running?
129
- Console.logger.warn(self) {"Group is still running after stopping it!"}
134
+ Console.warn(self) {"Group is still running after stopping it!"}
130
135
  end
131
136
  ensure
132
137
  @running = true
@@ -140,7 +145,7 @@ module Async
140
145
  name ||= UNNAMED
141
146
 
142
147
  if mark?(key)
143
- Console.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
148
+ Console.debug(self) {"Reusing existing child for #{key}: #{name}"}
144
149
  return false
145
150
  end
146
151
 
@@ -161,10 +166,10 @@ module Async
161
166
  end
162
167
 
163
168
  if status.success?
164
- Console.logger.debug(self) {"#{child} exited with #{status}"}
169
+ Console.debug(self) {"#{child} exited with #{status}"}
165
170
  else
166
171
  @statistics.failure!
167
- Console.logger.error(self) {status}
172
+ Console.error(self, status: status)
168
173
  end
169
174
 
170
175
  if restart
@@ -190,8 +195,12 @@ module Async
190
195
 
191
196
  # @deprecated Please use {spawn} or {run} instead.
192
197
  def async(**options, &block)
198
+ # warn "#{self.class}##{__method__} is deprecated, please use `spawn` or `run` instead.", uplevel: 1
199
+
200
+ require "async"
201
+
193
202
  spawn(**options) do |instance|
194
- Async::Reactor.run(instance, &block)
203
+ Async(instance, &block)
195
204
  end
196
205
  end
197
206
 
@@ -3,10 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require 'fiber'
7
- require 'async/clock'
6
+ require "fiber"
7
+ require "async/clock"
8
8
 
9
- require_relative 'error'
9
+ require_relative "error"
10
10
 
11
11
  module Async
12
12
  module Container
@@ -65,7 +65,7 @@ module Async
65
65
  # Interrupt all running processes.
66
66
  # This resumes the controlling fiber with an instance of {Interrupt}.
67
67
  def interrupt
68
- Console.logger.debug(self, "Sending interrupt to #{@running.size} running processes...")
68
+ Console.info(self, "Sending interrupt to #{@running.size} running processes...")
69
69
  @running.each_value do |fiber|
70
70
  fiber.resume(Interrupt)
71
71
  end
@@ -74,7 +74,7 @@ module Async
74
74
  # Terminate all running processes.
75
75
  # This resumes the controlling fiber with an instance of {Terminate}.
76
76
  def terminate
77
- Console.logger.debug(self, "Sending terminate to #{@running.size} running processes...")
77
+ Console.info(self, "Sending terminate to #{@running.size} running processes...")
78
78
  @running.each_value do |fiber|
79
79
  fiber.resume(Terminate)
80
80
  end
@@ -83,6 +83,7 @@ module Async
83
83
  # Stop all child processes using {#terminate}.
84
84
  # @parameter timeout [Boolean | Numeric | Nil] If specified, invoke a graceful shutdown using {#interrupt} first.
85
85
  def stop(timeout = 1)
86
+ Console.info(self, "Stopping all processes...", timeout: timeout)
86
87
  # Use a default timeout if not specified:
87
88
  timeout = 1 if timeout == true
88
89
 
@@ -105,7 +106,7 @@ module Async
105
106
  end
106
107
 
107
108
  # Terminate all children:
108
- self.terminate
109
+ self.terminate if any?
109
110
 
110
111
  # Wait for all children to exit:
111
112
  self.wait
@@ -137,14 +138,24 @@ module Async
137
138
  protected
138
139
 
139
140
  def wait_for_children(duration = nil)
140
- Console.debug(self, "Waiting for children...", duration: duration)
141
+ Console.debug(self, "Waiting for children...", duration: duration, running: @running)
142
+
141
143
  if !@running.empty?
142
144
  # Maybe consider using a proper event loop here:
145
+ if ready = self.select(duration)
146
+ ready.each do |io|
147
+ @running[io].resume
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ # Wait for a child process to exit OR a signal to be received.
154
+ def select(duration)
155
+ ::Thread.handle_interrupt(SignalException => :immediate) do
143
156
  readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)
144
157
 
145
- readable&.each do |io|
146
- @running[io].resume
147
- end
158
+ return readable
148
159
  end
149
160
  end
150
161
 
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
  # Copyright, 2022, by Anton Sozontov.
6
6
 
7
- require_relative 'forked'
8
- require_relative 'threaded'
7
+ require_relative "forked"
8
+ require_relative "threaded"
9
9
 
10
10
  module Async
11
11
  module Container
@@ -3,9 +3,9 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative 'client'
6
+ require_relative "client"
7
7
 
8
- require 'console/logger'
8
+ require "console"
9
9
 
10
10
  module Async
11
11
  module Container
@@ -24,8 +24,8 @@ module Async
24
24
  end
25
25
 
26
26
  # Send a message to the console.
27
- def send(level: :debug, **message)
28
- @logger.send(level, self) {message}
27
+ def send(level: :info, **message)
28
+ @logger.public_send(level, self) {message}
29
29
  end
30
30
 
31
31
  # Send an error message to the console.