async-container 0.16.6 → 0.16.11

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: cffebcdeda8a463f4bd854a2023b6942dfd56f7c0d4985555c5b2fc8ef6b5f93
4
- data.tar.gz: a39ea00fceed362028c189a687372d0214b2c1da0b7b88aa16cbd4375d19b75a
3
+ metadata.gz: 0b6ba3c06851500a3d47b2fc72147caf9dc34fa0fd0cd759c7c88f36df808987
4
+ data.tar.gz: cd43c233e26d54801270ec66b797f84dc7309c53f3b8aef3af5a75bcf484c5c4
5
5
  SHA512:
6
- metadata.gz: b3939f550483a2330876833d50b9e79e604e81ca42160190d15b6bfb9ac213a3b4ee8851fb0f079402ffb32e5c14b29563d6beedc4cdec337d975af2bec15fc9
7
- data.tar.gz: 9afebedefb06016327f5f3c88a134dc825cfbd52d1206822d8a430cc7498a6e5a90ebb4996b7157e9388f0c089c2b8135e98f188a57ef71029de190d79b05fa1
6
+ metadata.gz: a726816975444a5eb5b92569970814e2bc997814466d86c08a4053adf8304a6f13036955dff3718f80d5fe43ef9195491be3045e54f1bdb247838199759062b8
7
+ data.tar.gz: d0dd11f404e8ddb9c5dcb05d996651dbbde1a61ed404ef241df1c81ee5e28ecfd09c80504b6ddf8465c425dcccddca65d6f52d1c4e87dda9141b21b4b366b27f
@@ -23,7 +23,6 @@
23
23
  require_relative 'container/controller'
24
24
 
25
25
  module Async
26
- # Containers execute one or more "instances" which typically contain a reactor. A container spawns "instances" using threads and/or processes. Because these are resources that must be cleaned up some how (either by `join` or `waitpid`), their creation is deferred until the user invokes `Container#wait`. When executed this way, the container guarantees that all "instances" will be complete once `Container#wait` returns. Containers are constructs for achieving parallelism, and are not designed to be used directly for concurrency. Typically, you'd create one or more container, add some tasks to it, and then wait for it to complete.
27
26
  module Container
28
27
  end
29
28
  end
@@ -25,12 +25,16 @@ require_relative 'threaded'
25
25
  require_relative 'hybrid'
26
26
 
27
27
  module Async
28
- # Containers execute one or more "instances" which typically contain a reactor. A container spawns "instances" using threads and/or processes. Because these are resources that must be cleaned up some how (either by `join` or `waitpid`), their creation is deferred until the user invokes `Container#wait`. When executed this way, the container guarantees that all "instances" will be complete once `Container#wait` returns. Containers are constructs for achieving parallelism, and are not designed to be used directly for concurrency. Typically, you'd create one or more container, add some tasks to it, and then wait for it to complete.
29
28
  module Container
29
+ # Whether the underlying process supports fork.
30
+ # @returns [Boolean]
30
31
  def self.fork?
31
32
  ::Process.respond_to?(:fork) && ::Process.respond_to?(:setpgid)
32
33
  end
33
34
 
35
+ # Determins the best container class based on the underlying Ruby implementation.
36
+ # Some platforms, including JRuby, don't support fork. Applications which just want a reasonable default can use this method.
37
+ # @returns [Class]
34
38
  def self.best_container_class
35
39
  if fork?
36
40
  return Forked
@@ -39,8 +43,10 @@ module Async
39
43
  end
40
44
  end
41
45
 
42
- def self.new(*arguments)
43
- best_container_class.new(*arguments)
46
+ # Create an instance of the best container class.
47
+ # @returns [Generic] Typically an instance of either {Forked} or {Threaded} containers.
48
+ def self.new(*arguments, **options)
49
+ best_container_class.new(*arguments, **options)
44
50
  end
45
51
  end
46
52
  end
@@ -24,27 +24,40 @@ require 'json'
24
24
 
25
25
  module Async
26
26
  module Container
27
+ # Provides a basic multi-thread/multi-process uni-directional communication channel.
27
28
  class Channel
29
+ # Initialize the channel using a pipe.
28
30
  def initialize
29
31
  @in, @out = ::IO.pipe
30
32
  end
31
33
 
34
+ # The input end of the pipe.
35
+ # @attribute [IO]
32
36
  attr :in
37
+
38
+ # The output end of the pipe.
39
+ # @attribute [IO]
33
40
  attr :out
34
41
 
42
+ # Close the input end of the pipe.
35
43
  def close_read
36
44
  @in.close
37
45
  end
38
46
 
47
+ # Close the output end of the pipe.
39
48
  def close_write
40
49
  @out.close
41
50
  end
42
51
 
52
+ # Close both ends of the pipe.
43
53
  def close
44
54
  close_read
45
55
  close_write
46
56
  end
47
57
 
58
+ # Receive an object from the pipe.
59
+ # Internally, prefers to receive newline formatted JSON, otherwise returns a hash table with a single key `:line` which contains the line of data that could not be parsed as JSON.
60
+ # @returns [Hash]
48
61
  def receive
49
62
  if data = @in.gets
50
63
  begin
@@ -28,17 +28,8 @@ require_relative 'notify'
28
28
 
29
29
  module Async
30
30
  module Container
31
- class InitializationError < Error
32
- def initialize(container)
33
- super("Could not create container!")
34
-
35
- @container = container
36
- end
37
-
38
- attr :container
39
- end
40
-
41
- # Manages the life-cycle of a container.
31
+ # Manages the life-cycle of one or more containers in order to support a persistent system.
32
+ # e.g. a web server, job server or some other long running system.
42
33
  class Controller
43
34
  SIGHUP = Signal.list["HUP"]
44
35
  SIGINT = Signal.list["INT"]
@@ -46,6 +37,8 @@ module Async
46
37
  SIGUSR1 = Signal.list["USR1"]
47
38
  SIGUSR2 = Signal.list["USR2"]
48
39
 
40
+ # Initialize the controller.
41
+ # @parameter notify [Notify::Client] A client used for process readiness notifications.
49
42
  def initialize(notify: Notify.open!)
50
43
  @container = nil
51
44
 
@@ -60,6 +53,8 @@ module Async
60
53
  end
61
54
  end
62
55
 
56
+ # The state of the controller.
57
+ # @returns [String]
63
58
  def state_string
64
59
  if running?
65
60
  "running"
@@ -68,49 +63,68 @@ module Async
68
63
  end
69
64
  end
70
65
 
66
+ # A human readable representation of the controller.
67
+ # @returns [String]
71
68
  def to_s
72
69
  "#{self.class} #{state_string}"
73
70
  end
74
71
 
72
+ # Trap the specified signal.
73
+ # @parameters signal [Symbol] The signal to trap, e.g. `:INT`.
74
+ # @parameters block [Proc] The signal handler to invoke.
75
75
  def trap(signal, &block)
76
76
  @signals[signal] = block
77
77
  end
78
78
 
79
+ # The current container being managed by the controller.
79
80
  attr :container
80
81
 
82
+ # Create a container for the controller.
83
+ # Can be overridden by a sub-class.
84
+ # @returns [Generic] A specific container instance to use.
81
85
  def create_container
82
86
  Container.new
83
87
  end
84
88
 
89
+ # Whether the controller has a running container.
90
+ # @returns [Boolean]
85
91
  def running?
86
92
  !!@container
87
93
  end
88
94
 
95
+ # Wait for the underlying container to start.
89
96
  def wait
90
97
  @container&.wait
91
98
  end
92
99
 
100
+ # Spawn container instances into the given container.
101
+ # Should be overridden by a sub-class.
102
+ # @parameter container [Generic] The container, generally from {#create_container}.
93
103
  def setup(container)
94
104
  # Don't do this, otherwise calling super is risky for sub-classes:
95
105
  # raise NotImplementedError, "Container setup is must be implemented in derived class!"
96
106
  end
97
107
 
108
+ # Start the container unless it's already running.
98
109
  def start
99
110
  self.restart unless @container
100
111
  end
101
112
 
113
+ # Stop the container if it's running.
114
+ # @parameter graceful [Boolean] Whether to give the children instances time to shut down or to kill them immediately.
102
115
  def stop(graceful = true)
103
116
  @container&.stop(graceful)
104
117
  @container = nil
105
118
  end
106
119
 
120
+ # Restart the container. A new container is created, and if successful, any old container is terminated gracefully.
107
121
  def restart
108
122
  if @container
109
123
  @notify&.restarting!
110
124
 
111
- Async.logger.debug(self) {"Restarting container..."}
125
+ Console.logger.debug(self) {"Restarting container..."}
112
126
  else
113
- Async.logger.debug(self) {"Starting container..."}
127
+ Console.logger.debug(self) {"Starting container..."}
114
128
  end
115
129
 
116
130
  container = self.create_container
@@ -120,27 +134,27 @@ module Async
120
134
  rescue
121
135
  @notify&.error!($!.to_s)
122
136
 
123
- raise InitializationError, container
137
+ raise SetupError, container
124
138
  end
125
139
 
126
140
  # Wait for all child processes to enter the ready state.
127
- Async.logger.debug(self, "Waiting for startup...")
141
+ Console.logger.debug(self, "Waiting for startup...")
128
142
  container.wait_until_ready
129
- Async.logger.debug(self, "Finished startup.")
143
+ Console.logger.debug(self, "Finished startup.")
130
144
 
131
145
  if container.failed?
132
146
  @notify&.error!($!.to_s)
133
147
 
134
148
  container.stop
135
149
 
136
- raise InitializationError, container
150
+ raise SetupError, container
137
151
  end
138
152
 
139
153
  # Make this swap as atomic as possible:
140
154
  old_container = @container
141
155
  @container = container
142
156
 
143
- Async.logger.debug(self, "Stopping old container...")
157
+ Console.logger.debug(self, "Stopping old container...")
144
158
  old_container&.stop
145
159
  @notify&.ready!
146
160
  rescue
@@ -150,31 +164,33 @@ module Async
150
164
  raise
151
165
  end
152
166
 
167
+ # Reload the existing container. Children instances will be reloaded using `SIGHUP`.
153
168
  def reload
154
169
  @notify&.reloading!
155
170
 
156
- Async.logger.info(self) {"Reloading container: #{@container}..."}
171
+ Console.logger.info(self) {"Reloading container: #{@container}..."}
157
172
 
158
173
  begin
159
174
  self.setup(@container)
160
175
  rescue
161
- raise InitializationError, container
176
+ raise SetupError, container
162
177
  end
163
178
 
164
179
  # Wait for all child processes to enter the ready state.
165
- Async.logger.debug(self, "Waiting for startup...")
180
+ Console.logger.debug(self, "Waiting for startup...")
166
181
  @container.wait_until_ready
167
- Async.logger.debug(self, "Finished startup.")
182
+ Console.logger.debug(self, "Finished startup.")
168
183
 
169
184
  if @container.failed?
170
185
  @notify.error!("Container failed!")
171
186
 
172
- raise InitializationError, @container
187
+ raise SetupError, @container
173
188
  else
174
189
  @notify&.ready!
175
190
  end
176
191
  end
177
192
 
193
+ # Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.
178
194
  def run
179
195
  # I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
180
196
  interrupt_action = Signal.trap(:INT) do
@@ -185,6 +201,10 @@ module Async
185
201
  raise Terminate
186
202
  end
187
203
 
204
+ hangup_action = Signal.trap(:HUP) do
205
+ raise Hangup
206
+ end
207
+
188
208
  self.start
189
209
 
190
210
  while @container&.running?
@@ -194,8 +214,8 @@ module Async
194
214
  if handler = @signals[exception.signo]
195
215
  begin
196
216
  handler.call
197
- rescue InitializationError => error
198
- Async.logger.error(self) {error}
217
+ rescue SetupError => error
218
+ Console.logger.error(self) {error}
199
219
  end
200
220
  else
201
221
  raise
@@ -212,6 +232,7 @@ module Async
212
232
  # Restore the interrupt handler:
213
233
  Signal.trap(:INT, interrupt_action)
214
234
  Signal.trap(:TERM, terminate_action)
235
+ Signal.trap(:HUP, hangup_action)
215
236
  end
216
237
  end
217
238
  end
@@ -27,6 +27,7 @@ module Async
27
27
 
28
28
  Interrupt = ::Interrupt
29
29
 
30
+ # Similar to {Interrupt}, but represents `SIGTERM`.
30
31
  class Terminate < SignalException
31
32
  SIGTERM = Signal.list['TERM']
32
33
 
@@ -34,5 +35,25 @@ module Async
34
35
  super(SIGTERM)
35
36
  end
36
37
  end
38
+
39
+ class Hangup < SignalException
40
+ SIGHUP = Signal.list['HUP']
41
+
42
+ def initialize
43
+ super(SIGHUP)
44
+ end
45
+ end
46
+
47
+ # Represents the error which occured when a container failed to start up correctly.
48
+ class SetupError < Error
49
+ def initialize(container)
50
+ super("Could not create container!")
51
+
52
+ @container = container
53
+ end
54
+
55
+ # The container that failed.
56
+ attr :container
57
+ end
37
58
  end
38
59
  end
@@ -24,13 +24,17 @@ require_relative 'generic'
24
24
  require_relative 'process'
25
25
 
26
26
  module Async
27
- # Manages a reactor within one or more threads.
28
27
  module Container
28
+ # A multi-process container which uses {Process.fork}.
29
29
  class Forked < Generic
30
+ # Indicates that this is a multi-process container.
30
31
  def self.multiprocess?
31
32
  true
32
33
  end
33
34
 
35
+ # Start a named child process and execute the provided block in it.
36
+ # @parameter name [String] The name (title) of the child process.
37
+ # @parameter block [Proc] The block to execute in the child process.
34
38
  def start(name, &block)
35
39
  Process.fork(name: name, &block)
36
40
  end
@@ -30,10 +30,12 @@ require_relative 'statistics'
30
30
 
31
31
  module Async
32
32
  module Container
33
+ # An environment variable key to override {.processor_count}.
33
34
  ASYNC_CONTAINER_PROCESSOR_COUNT = 'ASYNC_CONTAINER_PROCESSOR_COUNT'
34
35
 
35
- # 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.
36
- # @return [Integer] the number of hardware processors which can run threads/processes simultaneously.
36
+ # 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.
37
+ # @returns [Integer] The number of hardware processors which can run threads/processes simultaneously.
38
+ # @raises [RuntimeError] If the process count is invalid.
37
39
  def self.processor_count(env = ENV)
38
40
  count = env.fetch(ASYNC_CONTAINER_PROCESSOR_COUNT) do
39
41
  Etc.nprocessors rescue 1
@@ -46,6 +48,7 @@ module Async
46
48
  return count
47
49
  end
48
50
 
51
+ # A base class for implementing containers.
49
52
  class Generic
50
53
  def self.run(*arguments, **options, &block)
51
54
  self.new.run(*arguments, **options, &block)
@@ -65,27 +68,35 @@ module Async
65
68
 
66
69
  attr :state
67
70
 
71
+ # A human readable representation of the container.
72
+ # @returns [String]
68
73
  def to_s
69
74
  "#{self.class} with #{@statistics.spawns} spawns and #{@statistics.failures} failures."
70
75
  end
71
76
 
77
+ # Look up a child process by key.
78
+ # A key could be a symbol, a file path, or something else which the child instance represents.
72
79
  def [] key
73
80
  @keyed[key]&.value
74
81
  end
75
82
 
83
+ # Statistics relating to the behavior of children instances.
84
+ # @attribute [Statistics]
76
85
  attr :statistics
77
86
 
87
+ # Whether any failures have occurred within the container.
88
+ # @returns [Boolean]
78
89
  def failed?
79
90
  @statistics.failed?
80
91
  end
81
92
 
82
- # Whether there are running tasks.
93
+ # Whether the container has running children instances.
83
94
  def running?
84
95
  @group.running?
85
96
  end
86
97
 
87
98
  # Sleep until some state change occurs.
88
- # @param duration [Integer] the maximum amount of time to sleep for.
99
+ # @parameter duration [Numeric] the maximum amount of time to sleep for.
89
100
  def sleep(duration = nil)
90
101
  @group.sleep(duration)
91
102
  end
@@ -95,14 +106,20 @@ module Async
95
106
  @group.wait
96
107
  end
97
108
 
109
+ # Returns true if all children instances have the specified status flag set.
110
+ # e.g. `:ready`.
111
+ # This state is updated by the process readiness protocol mechanism. See {Notify::Client} for more details.
112
+ # @returns [Boolean]
98
113
  def status?(flag)
99
114
  # This also returns true if all processes have exited/failed:
100
115
  @state.all?{|_, state| state[flag]}
101
116
  end
102
117
 
118
+ # Wait until all the children instances have indicated that they are ready.
119
+ # @returns [Boolean] The children all became ready.
103
120
  def wait_until_ready
104
121
  while true
105
- Async.logger.debug(self) do |buffer|
122
+ Console.logger.debug(self) do |buffer|
106
123
  buffer.puts "Waiting for ready:"
107
124
  @state.each do |child, state|
108
125
  buffer.puts "\t#{child.class}: #{state.inspect}"
@@ -117,28 +134,34 @@ module Async
117
134
  end
118
135
  end
119
136
 
137
+ # Stop the children instances.
138
+ # @parameter timeout [Boolean | Numeric] Whether to stop gracefully, or a specific timeout.
120
139
  def stop(timeout = true)
121
140
  @running = false
122
141
  @group.stop(timeout)
123
142
 
124
143
  if @group.running?
125
- Async.logger.warn(self) {"Group is still running after stopping it!"}
144
+ Console.logger.warn(self) {"Group is still running after stopping it!"}
126
145
  end
127
146
  ensure
128
147
  @running = true
129
148
  end
130
149
 
150
+ # Spawn a child instance into the container.
151
+ # @parameter name [String] The name of the child instance.
152
+ # @parameter restart [Boolean] Whether to restart the child instance if it fails.
153
+ # @parameter key [Symbol] A key used for reloading child instances.
131
154
  def spawn(name: nil, restart: false, key: nil, &block)
132
155
  name ||= UNNAMED
133
156
 
134
157
  if mark?(key)
135
- Async.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
158
+ Console.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
136
159
  return false
137
160
  end
138
161
 
139
162
  @statistics.spawn!
140
163
 
141
- Fiber.new do
164
+ fiber do
142
165
  while @running
143
166
  child = self.start(name, &block)
144
167
 
@@ -153,10 +176,10 @@ module Async
153
176
  end
154
177
 
155
178
  if status.success?
156
- Async.logger.info(self) {"#{child} exited with #{status}"}
179
+ Console.logger.info(self) {"#{child} exited with #{status}"}
157
180
  else
158
181
  @statistics.failure!
159
- Async.logger.error(self) {status}
182
+ Console.logger.error(self) {status}
160
183
  end
161
184
 
162
185
  if restart
@@ -166,18 +189,14 @@ module Async
166
189
  end
167
190
  end
168
191
  # ensure
169
- # Async.logger.error(self) {$!} if $!
192
+ # Console.logger.error(self) {$!} if $!
170
193
  end.resume
171
194
 
172
195
  return true
173
196
  end
174
197
 
175
- def async(**options, &block)
176
- spawn(**options) do |instance|
177
- Async::Reactor.run(instance, &block)
178
- end
179
- end
180
-
198
+ # Run multiple instances of the same block in the container.
199
+ # @parameter count [Integer] The number of instances to start.
181
200
  def run(count: Container.processor_count, **options, &block)
182
201
  count.times do
183
202
  spawn(**options, &block)
@@ -186,6 +205,14 @@ module Async
186
205
  return self
187
206
  end
188
207
 
208
+ # @deprecated Please use {spawn} or {run} instead.
209
+ def async(**options, &block)
210
+ spawn(**options) do |instance|
211
+ Async::Reactor.run(instance, &block)
212
+ end
213
+ end
214
+
215
+ # Reload the container's keyed instances.
189
216
  def reload
190
217
  @keyed.each_value(&:clear!)
191
218
 
@@ -200,6 +227,7 @@ module Async
200
227
  return dirty
201
228
  end
202
229
 
230
+ # Mark the container's keyed instance which ensures that it won't be discarded.
203
231
  def mark?(key)
204
232
  if key
205
233
  if value = @keyed[key]
@@ -212,6 +240,7 @@ module Async
212
240
  return false
213
241
  end
214
242
 
243
+ # Whether a child instance exists for the given key.
215
244
  def key?(key)
216
245
  if key
217
246
  @keyed.key?(key)
@@ -241,6 +270,18 @@ module Async
241
270
 
242
271
  @state.delete(child)
243
272
  end
273
+
274
+ private
275
+
276
+ if Fiber.respond_to?(:blocking?)
277
+ def fiber(&block)
278
+ Fiber.new(blocking: true, &block)
279
+ end
280
+ else
281
+ def fiber(&block)
282
+ Fiber.new(&block)
283
+ end
284
+ end
244
285
  end
245
286
  end
246
287
  end