async-container 0.18.3 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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.