async-container 0.16.4 → 0.16.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/lib/async/container.rb +0 -1
  3. data/lib/async/container/best.rb +9 -3
  4. data/lib/async/container/channel.rb +13 -0
  5. data/lib/async/container/controller.rb +49 -25
  6. data/lib/async/container/error.rb +21 -0
  7. data/lib/async/container/forked.rb +5 -1
  8. data/lib/async/container/generic.rb +57 -19
  9. data/lib/async/container/group.rb +19 -2
  10. data/lib/async/container/hybrid.rb +16 -2
  11. data/lib/async/container/keyed.rb +12 -0
  12. data/lib/async/container/notify.rb +4 -5
  13. data/lib/async/container/notify/client.rb +16 -1
  14. data/lib/async/container/notify/console.rb +8 -21
  15. data/lib/async/container/notify/pipe.rb +8 -22
  16. data/lib/async/container/notify/server.rb +0 -1
  17. data/lib/async/container/notify/socket.rb +14 -1
  18. data/lib/async/container/process.rb +28 -2
  19. data/lib/async/container/statistics.rb +16 -0
  20. data/lib/async/container/thread.rb +42 -6
  21. data/lib/async/container/threaded.rb +5 -1
  22. data/lib/async/container/version.rb +1 -1
  23. metadata +13 -88
  24. data/.editorconfig +0 -6
  25. data/.github/workflows/development.yml +0 -36
  26. data/.gitignore +0 -21
  27. data/.rspec +0 -3
  28. data/.travis.yml +0 -21
  29. data/.yardopts +0 -1
  30. data/Gemfile +0 -19
  31. data/Guardfile +0 -14
  32. data/README.md +0 -140
  33. data/Rakefile +0 -8
  34. data/async-container.gemspec +0 -34
  35. data/examples/async.rb +0 -22
  36. data/examples/channel.rb +0 -45
  37. data/examples/channels/client.rb +0 -103
  38. data/examples/container.rb +0 -33
  39. data/examples/isolate.rb +0 -36
  40. data/examples/minimal.rb +0 -94
  41. data/examples/test.rb +0 -51
  42. data/examples/threads.rb +0 -25
  43. data/examples/title.rb +0 -13
  44. data/examples/udppipe.rb +0 -35
  45. data/spec/async/container/controller_spec.rb +0 -105
  46. data/spec/async/container/dots.rb +0 -34
  47. data/spec/async/container/forked_spec.rb +0 -61
  48. data/spec/async/container/hybrid_spec.rb +0 -36
  49. data/spec/async/container/notify/notify.rb +0 -19
  50. data/spec/async/container/notify/pipe_spec.rb +0 -48
  51. data/spec/async/container/notify_spec.rb +0 -56
  52. data/spec/async/container/shared_examples.rb +0 -80
  53. data/spec/async/container/signal_spec.rb +0 -66
  54. data/spec/async/container/threaded_spec.rb +0 -35
  55. data/spec/async/container_spec.rb +0 -41
  56. data/spec/spec_helper.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5d86c65b350b653e621c4f36e8005fc85ba7c6a97569784dc2d5170d1310a27
4
- data.tar.gz: 708b959103791bb424fae6c8f32e8a213dc04986c4e33e498069ebcd08b12473
3
+ metadata.gz: a76bd927dba80c9eb45b6e0c4ba41c463bfcbe1ffa90ff00b1cef7375e522670
4
+ data.tar.gz: 41aa83bb3be7be3ab3cdd8f2f7065a4960b800acdd39d5c89f2c420d70cc312b
5
5
  SHA512:
6
- metadata.gz: 5b216348b1514841d813e1455565b41e108a9c284e2b26e2629d8a8cf26acf7c5952bc0e63a2a84ab2c144b6a4c462cbdb1e1b54255ed1ecdbc4107a92d0e66b
7
- data.tar.gz: f0afa2fcb3cd7ee73dd67cc92d0b004bc7b467cef2b7f4753ddbd296316d6364e81f7edd22987887957e14fe5bb0971370263e84731b7ebb181a109e635ea5e1
6
+ metadata.gz: 2c7878995dea584f4c5284214a5d96074ada191a2d54d07cc7f1dc6815e03edbc60ff729bc5f377c11a1ed00f531019aabefe89b13f04e7d305edcd6110a6770
7
+ data.tar.gz: ac4e0f6e39e315311283a71a8a24cfc64980c352c21bdc0a33fdcfd4217d4564b72229450583000263b814a668bbb1ae81cfa3c420e001ec042da58ccf650d75
@@ -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
 
@@ -55,9 +48,13 @@ module Async
55
48
 
56
49
  @signals = {}
57
50
 
58
- trap(SIGHUP, &self.method(:restart))
51
+ trap(SIGHUP) do
52
+ self.restart
53
+ end
59
54
  end
60
55
 
56
+ # The state of the controller.
57
+ # @returns [String]
61
58
  def state_string
62
59
  if running?
63
60
  "running"
@@ -66,49 +63,68 @@ module Async
66
63
  end
67
64
  end
68
65
 
66
+ # A human readable representation of the controller.
67
+ # @returns [String]
69
68
  def to_s
70
69
  "#{self.class} #{state_string}"
71
70
  end
72
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.
73
75
  def trap(signal, &block)
74
76
  @signals[signal] = block
75
77
  end
76
78
 
79
+ # The current container being managed by the controller.
77
80
  attr :container
78
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.
79
85
  def create_container
80
86
  Container.new
81
87
  end
82
88
 
89
+ # Whether the controller has a running container.
90
+ # @returns [Boolean]
83
91
  def running?
84
92
  !!@container
85
93
  end
86
94
 
95
+ # Wait for the underlying container to start.
87
96
  def wait
88
97
  @container&.wait
89
98
  end
90
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}.
91
103
  def setup(container)
92
104
  # Don't do this, otherwise calling super is risky for sub-classes:
93
105
  # raise NotImplementedError, "Container setup is must be implemented in derived class!"
94
106
  end
95
107
 
108
+ # Start the container unless it's already running.
96
109
  def start
97
110
  self.restart unless @container
98
111
  end
99
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.
100
115
  def stop(graceful = true)
101
116
  @container&.stop(graceful)
102
117
  @container = nil
103
118
  end
104
119
 
120
+ # Restart the container. A new container is created, and if successful, any old container is terminated gracefully.
105
121
  def restart
106
122
  if @container
107
123
  @notify&.restarting!
108
124
 
109
- Async.logger.debug(self) {"Restarting container..."}
125
+ Console.logger.debug(self) {"Restarting container..."}
110
126
  else
111
- Async.logger.debug(self) {"Starting container..."}
127
+ Console.logger.debug(self) {"Starting container..."}
112
128
  end
113
129
 
114
130
  container = self.create_container
@@ -118,26 +134,27 @@ module Async
118
134
  rescue
119
135
  @notify&.error!($!.to_s)
120
136
 
121
- raise InitializationError, container
137
+ raise SetupError, container
122
138
  end
123
139
 
124
140
  # Wait for all child processes to enter the ready state.
125
- Async.logger.debug(self, "Waiting for startup...")
141
+ Console.logger.debug(self, "Waiting for startup...")
126
142
  container.wait_until_ready
127
- Async.logger.debug(self, "Finished startup.")
143
+ Console.logger.debug(self, "Finished startup.")
128
144
 
129
145
  if container.failed?
130
146
  @notify&.error!($!.to_s)
131
147
 
132
148
  container.stop
133
149
 
134
- raise InitializationError, container
150
+ raise SetupError, container
135
151
  end
136
152
 
137
153
  # Make this swap as atomic as possible:
138
154
  old_container = @container
139
155
  @container = container
140
156
 
157
+ Console.logger.debug(self, "Stopping old container...")
141
158
  old_container&.stop
142
159
  @notify&.ready!
143
160
  rescue
@@ -147,31 +164,33 @@ module Async
147
164
  raise
148
165
  end
149
166
 
167
+ # Reload the existing container. Children instances will be reloaded using `SIGHUP`.
150
168
  def reload
151
169
  @notify&.reloading!
152
170
 
153
- Async.logger.info(self) {"Reloading container: #{@container}..."}
171
+ Console.logger.info(self) {"Reloading container: #{@container}..."}
154
172
 
155
173
  begin
156
174
  self.setup(@container)
157
175
  rescue
158
- raise InitializationError, container
176
+ raise SetupError, container
159
177
  end
160
178
 
161
179
  # Wait for all child processes to enter the ready state.
162
- Async.logger.debug(self, "Waiting for startup...")
180
+ Console.logger.debug(self, "Waiting for startup...")
163
181
  @container.wait_until_ready
164
- Async.logger.debug(self, "Finished startup.")
182
+ Console.logger.debug(self, "Finished startup.")
165
183
 
166
184
  if @container.failed?
167
185
  @notify.error!("Container failed!")
168
186
 
169
- raise InitializationError, @container
187
+ raise SetupError, @container
170
188
  else
171
189
  @notify&.ready!
172
190
  end
173
191
  end
174
192
 
193
+ # Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.
175
194
  def run
176
195
  # I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
177
196
  interrupt_action = Signal.trap(:INT) do
@@ -182,6 +201,10 @@ module Async
182
201
  raise Terminate
183
202
  end
184
203
 
204
+ hangup_action = Signal.trap(:HUP) do
205
+ raise Hangup
206
+ end
207
+
185
208
  self.start
186
209
 
187
210
  while @container&.running?
@@ -191,8 +214,8 @@ module Async
191
214
  if handler = @signals[exception.signo]
192
215
  begin
193
216
  handler.call
194
- rescue InitializationError => error
195
- Async.logger.error(self) {error}
217
+ rescue SetupError => error
218
+ Console.logger.error(self) {error}
196
219
  end
197
220
  else
198
221
  raise
@@ -209,6 +232,7 @@ module Async
209
232
  # Restore the interrupt handler:
210
233
  Signal.trap(:INT, interrupt_action)
211
234
  Signal.trap(:TERM, terminate_action)
235
+ Signal.trap(:HUP, hangup_action)
212
236
  end
213
237
  end
214
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,13 +30,25 @@ require_relative 'statistics'
30
30
 
31
31
  module Async
32
32
  module Container
33
- # @return [Integer] the number of hardware processors which can run threads/processes simultaneously.
34
- def self.processor_count
35
- Etc.nprocessors
36
- rescue
37
- 2
33
+ # An environment variable key to override {.processor_count}.
34
+ ASYNC_CONTAINER_PROCESSOR_COUNT = 'ASYNC_CONTAINER_PROCESSOR_COUNT'
35
+
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.
39
+ def self.processor_count(env = ENV)
40
+ count = env.fetch(ASYNC_CONTAINER_PROCESSOR_COUNT) do
41
+ Etc.nprocessors rescue 1
42
+ end.to_i
43
+
44
+ if count < 1
45
+ raise RuntimeError, "Invalid processor count #{count}!"
46
+ end
47
+
48
+ return count
38
49
  end
39
50
 
51
+ # A base class for implementing containers.
40
52
  class Generic
41
53
  def self.run(*arguments, **options, &block)
42
54
  self.new.run(*arguments, **options, &block)
@@ -56,27 +68,35 @@ module Async
56
68
 
57
69
  attr :state
58
70
 
71
+ # A human readable representation of the container.
72
+ # @returns [String]
59
73
  def to_s
60
74
  "#{self.class} with #{@statistics.spawns} spawns and #{@statistics.failures} failures."
61
75
  end
62
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.
63
79
  def [] key
64
80
  @keyed[key]&.value
65
81
  end
66
82
 
83
+ # Statistics relating to the behavior of children instances.
84
+ # @attribute [Statistics]
67
85
  attr :statistics
68
86
 
87
+ # Whether any failures have occurred within the container.
88
+ # @returns [Boolean]
69
89
  def failed?
70
90
  @statistics.failed?
71
91
  end
72
92
 
73
- # Whether there are running tasks.
93
+ # Whether the container has running children instances.
74
94
  def running?
75
95
  @group.running?
76
96
  end
77
97
 
78
98
  # Sleep until some state change occurs.
79
- # @param duration [Integer] the maximum amount of time to sleep for.
99
+ # @parameter duration [Numeric] the maximum amount of time to sleep for.
80
100
  def sleep(duration = nil)
81
101
  @group.sleep(duration)
82
102
  end
@@ -86,14 +106,20 @@ module Async
86
106
  @group.wait
87
107
  end
88
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]
89
113
  def status?(flag)
90
114
  # This also returns true if all processes have exited/failed:
91
115
  @state.all?{|_, state| state[flag]}
92
116
  end
93
117
 
118
+ # Wait until all the children instances have indicated that they are ready.
119
+ # @returns [Boolean] The children all became ready.
94
120
  def wait_until_ready
95
121
  while true
96
- Async.logger.debug(self) do |buffer|
122
+ Console.logger.debug(self) do |buffer|
97
123
  buffer.puts "Waiting for ready:"
98
124
  @state.each do |child, state|
99
125
  buffer.puts "\t#{child.class}: #{state.inspect}"
@@ -108,22 +134,28 @@ module Async
108
134
  end
109
135
  end
110
136
 
137
+ # Stop the children instances.
138
+ # @parameter timeout [Boolean | Numeric] Whether to stop gracefully, or a specific timeout.
111
139
  def stop(timeout = true)
112
140
  @running = false
113
141
  @group.stop(timeout)
114
142
 
115
143
  if @group.running?
116
- Async.logger.warn(self) {"Group is still running after stopping it!"}
144
+ Console.logger.warn(self) {"Group is still running after stopping it!"}
117
145
  end
118
146
  ensure
119
147
  @running = true
120
148
  end
121
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.
122
154
  def spawn(name: nil, restart: false, key: nil, &block)
123
155
  name ||= UNNAMED
124
156
 
125
157
  if mark?(key)
126
- Async.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
158
+ Console.logger.debug(self) {"Reusing existing child for #{key}: #{name}"}
127
159
  return false
128
160
  end
129
161
 
@@ -144,10 +176,10 @@ module Async
144
176
  end
145
177
 
146
178
  if status.success?
147
- Async.logger.info(self) {"#{child} exited with #{status}"}
179
+ Console.logger.info(self) {"#{child} exited with #{status}"}
148
180
  else
149
181
  @statistics.failure!
150
- Async.logger.error(self) {status}
182
+ Console.logger.error(self) {status}
151
183
  end
152
184
 
153
185
  if restart
@@ -157,18 +189,14 @@ module Async
157
189
  end
158
190
  end
159
191
  # ensure
160
- # Async.logger.error(self) {$!} if $!
192
+ # Console.logger.error(self) {$!} if $!
161
193
  end.resume
162
194
 
163
195
  return true
164
196
  end
165
197
 
166
- def async(**options, &block)
167
- spawn(**options) do |instance|
168
- Async::Reactor.run(instance, &block)
169
- end
170
- end
171
-
198
+ # Run multiple instances of the same block in the container.
199
+ # @parameter count [Integer] The number of instances to start.
172
200
  def run(count: Container.processor_count, **options, &block)
173
201
  count.times do
174
202
  spawn(**options, &block)
@@ -177,6 +205,14 @@ module Async
177
205
  return self
178
206
  end
179
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.
180
216
  def reload
181
217
  @keyed.each_value(&:clear!)
182
218
 
@@ -191,6 +227,7 @@ module Async
191
227
  return dirty
192
228
  end
193
229
 
230
+ # Mark the container's keyed instance which ensures that it won't be discarded.
194
231
  def mark?(key)
195
232
  if key
196
233
  if value = @keyed[key]
@@ -203,6 +240,7 @@ module Async
203
240
  return false
204
241
  end
205
242
 
243
+ # Whether a child instance exists for the given key.
206
244
  def key?(key)
207
245
  if key
208
246
  @keyed.key?(key)