async-container 0.16.6 → 0.16.7

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: 87218f3196f6535fbb3849b6cb24ee3cc4ba6f59ed3c55ca9374b2889d51aa62
4
+ data.tar.gz: 3b38ac9e91231d51b0d29d7fab36c5cdae70df84833aa97307d6e4e9462b405d
5
5
  SHA512:
6
- metadata.gz: b3939f550483a2330876833d50b9e79e604e81ca42160190d15b6bfb9ac213a3b4ee8851fb0f079402ffb32e5c14b29563d6beedc4cdec337d975af2bec15fc9
7
- data.tar.gz: 9afebedefb06016327f5f3c88a134dc825cfbd52d1206822d8a430cc7498a6e5a90ebb4996b7157e9388f0c089c2b8135e98f188a57ef71029de190d79b05fa1
6
+ metadata.gz: 1c0c01247d61f0cdc5f5cf7aa404d2115ffecf853c1c33669dbd25dbed4d7e6f0db20495ffcf6052497156cfd16b41d7ec8319f7b57ee0ba6fda8aa799d28aa3
7
+ data.tar.gz: 6fa64afb47c7f8a83e79773ed9ca891966bf88e5ba092e304f859ffc2a76cd0525830f96ece65d9b1ca5637fc3527aa1750ff93b2374d807eca7642f68b73714
@@ -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,42 +63,61 @@ 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!
@@ -120,7 +134,7 @@ 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.
@@ -133,7 +147,7 @@ module Async
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:
@@ -150,6 +164,7 @@ 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
 
@@ -158,7 +173,7 @@ module Async
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.
@@ -169,12 +184,13 @@ module Async
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
@@ -194,7 +210,7 @@ module Async
194
210
  if handler = @signals[exception.signo]
195
211
  begin
196
212
  handler.call
197
- rescue InitializationError => error
213
+ rescue SetupError => error
198
214
  Async.logger.error(self) {error}
199
215
  end
200
216
  else
@@ -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,17 @@ module Async
34
35
  super(SIGTERM)
35
36
  end
36
37
  end
38
+
39
+ # Represents the error which occured when a container failed to start up correctly.
40
+ class SetupError < Error
41
+ def initialize(container)
42
+ super("Could not create container!")
43
+
44
+ @container = container
45
+ end
46
+
47
+ # The container that failed.
48
+ attr :container
49
+ end
37
50
  end
38
51
  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,11 +106,17 @@ 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
122
  Async.logger.debug(self) do |buffer|
@@ -117,6 +134,8 @@ 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)
@@ -128,6 +147,10 @@ module Async
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
 
@@ -172,12 +195,8 @@ module Async
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)
@@ -21,7 +21,6 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  require 'fiber'
24
-
25
24
  require 'async/clock'
26
25
 
27
26
  require_relative 'error'
@@ -30,6 +29,7 @@ module Async
30
29
  module Container
31
30
  # Manages a group of running processes.
32
31
  class Group
32
+ # Initialize an empty group.
33
33
  def initialize
34
34
  @running = {}
35
35
 
@@ -40,19 +40,25 @@ module Async
40
40
  # @attribute [Hash<IO, Fiber>] the running tasks, indexed by IO.
41
41
  attr :running
42
42
 
43
+ # Whether the group contains any running processes.
44
+ # @returns [Boolean]
43
45
  def running?
44
46
  @running.any?
45
47
  end
46
48
 
49
+ # Whether the group contains any running processes.
50
+ # @returns [Boolean]
47
51
  def any?
48
52
  @running.any?
49
53
  end
50
54
 
55
+ # Whether the group is empty.
56
+ # @returns [Boolean]
51
57
  def empty?
52
58
  @running.empty?
53
59
  end
54
60
 
55
- # This method sleeps for at most the specified duration.
61
+ # Sleep for at most the specified duration until some state change occurs.
56
62
  def sleep(duration)
57
63
  self.resume
58
64
  self.suspend
@@ -60,6 +66,7 @@ module Async
60
66
  self.wait_for_children(duration)
61
67
  end
62
68
 
69
+ # Begin any outstanding queued processes and wait for them indefinitely.
63
70
  def wait
64
71
  self.resume
65
72
 
@@ -68,6 +75,8 @@ module Async
68
75
  end
69
76
  end
70
77
 
78
+ # Interrupt all running processes.
79
+ # This resumes the controlling fiber with an instance of {Interrupt}.
71
80
  def interrupt
72
81
  Async.logger.debug(self, "Sending interrupt to #{@running.size} running processes...")
73
82
  @running.each_value do |fiber|
@@ -75,6 +84,8 @@ module Async
75
84
  end
76
85
  end
77
86
 
87
+ # Terminate all running processes.
88
+ # This resumes the controlling fiber with an instance of {Terminate}.
78
89
  def terminate
79
90
  Async.logger.debug(self, "Sending terminate to #{@running.size} running processes...")
80
91
  @running.each_value do |fiber|
@@ -82,6 +93,8 @@ module Async
82
93
  end
83
94
  end
84
95
 
96
+ # Stop all child processes using {#terminate}.
97
+ # @parameter timeout [Boolean | Numeric | Nil] If specified, invoke a graceful shutdown using {#interrupt} first.
85
98
  def stop(timeout = 1)
86
99
  # Use a default timeout if not specified:
87
100
  timeout = 1 if timeout == true
@@ -111,6 +124,7 @@ module Async
111
124
  self.wait
112
125
  end
113
126
 
127
+ # Wait for a message in the specified {Channel}.
114
128
  def wait_for(channel)
115
129
  io = channel.in
116
130
 
@@ -137,6 +151,7 @@ module Async
137
151
 
138
152
  def wait_for_children(duration = nil)
139
153
  if !@running.empty?
154
+ # Maybe consider using a proper event loop here:
140
155
  readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)
141
156
 
142
157
  readable&.each do |io|
@@ -25,7 +25,12 @@ require_relative 'threaded'
25
25
 
26
26
  module Async
27
27
  module Container
28
+ # Provides a hybrid multi-process multi-thread container.
28
29
  class Hybrid < Forked
30
+ # Run multiple instances of the same block in the container.
31
+ # @parameter count [Integer] The number of instances to start.
32
+ # @parameter forks [Integer] The number of processes to fork.
33
+ # @parameter threads [Integer] the number of threads to start.
29
34
  def run(count: nil, forks: nil, threads: nil, **options, &block)
30
35
  processor_count = Container.processor_count
31
36
  count ||= processor_count ** 2
@@ -22,6 +22,8 @@
22
22
 
23
23
  module Async
24
24
  module Container
25
+ # Tracks a key/value pair such that unmarked keys can be identified and cleaned up.
26
+ # This helps implement persistent processes that start up child processes per directory or configuration file. If those directories and/or configuration files are removed, the child process can then be cleaned up automatically, because those key/value pairs will not be marked when reloading the container.
25
27
  class Keyed
26
28
  def initialize(key, value)
27
29
  @key = key
@@ -29,21 +31,31 @@ module Async
29
31
  @marked = true
30
32
  end
31
33
 
34
+ # The key. Normally a symbol or a file-system path.
35
+ # @attribute [Object]
32
36
  attr :key
37
+
38
+ # The value. Normally a child instance of some sort.
39
+ # @attribute [Object]
33
40
  attr :value
34
41
 
42
+ # Has the instance been marked?
43
+ # @returns [Boolean]
35
44
  def marked?
36
45
  @marked
37
46
  end
38
47
 
48
+ # Mark the instance. This will indiciate that the value is still in use/active.
39
49
  def mark!
40
50
  @marked = true
41
51
  end
42
52
 
53
+ # Clear the instance. This is normally done before reloading a container.
43
54
  def clear!
44
55
  @marked = false
45
56
  end
46
57
 
58
+ # Stop the instance if it was not marked.
47
59
  def stop?
48
60
  unless @marked
49
61
  @value.stop
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -28,12 +27,12 @@ require_relative 'notify/console'
28
27
  module Async
29
28
  module Container
30
29
  module Notify
31
- # We cache the client on a per-process basis. Because that's the relevant scope for process readiness protocols.
32
- @@client = nil
30
+ @client = nil
33
31
 
32
+ # Select the best available notification client.
33
+ # We cache the client on a per-process basis. Because that's the relevant scope for process readiness protocols.
34
34
  def self.open!
35
- # Select the best available client:
36
- @@client ||= (
35
+ @client ||= (
37
36
  Pipe.open! ||
38
37
  Socket.open! ||
39
38
  Console.open!
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -23,12 +22,17 @@
23
22
 
24
23
  module Async
25
24
  module Container
25
+ # Handles the details of several process readiness protocols.
26
26
  module Notify
27
27
  class Client
28
+ # Notify the parent controller that the child has become ready, with a brief status message.
29
+ # @parameters message [Hash] Additional details to send with the message.
28
30
  def ready!(**message)
29
31
  send(ready: true, **message)
30
32
  end
31
33
 
34
+ # Notify the parent controller that the child is reloading.
35
+ # @parameters message [Hash] Additional details to send with the message.
32
36
  def reloading!(**message)
33
37
  message[:ready] = false
34
38
  message[:reloading] = true
@@ -37,6 +41,8 @@ module Async
37
41
  send(**message)
38
42
  end
39
43
 
44
+ # Notify the parent controller that the child is restarting.
45
+ # @parameters message [Hash] Additional details to send with the message.
40
46
  def restarting!(**message)
41
47
  message[:ready] = false
42
48
  message[:reloading] = true
@@ -45,14 +51,23 @@ module Async
45
51
  send(**message)
46
52
  end
47
53
 
54
+ # Notify the parent controller that the child is stopping.
55
+ # @parameters message [Hash] Additional details to send with the message.
48
56
  def stopping!(**message)
49
57
  message[:stopping] = true
58
+
59
+ send(**message)
50
60
  end
51
61
 
62
+ # Notify the parent controller of a status change.
63
+ # @parameters text [String] The details of the status change.
52
64
  def status!(text)
53
65
  send(status: text)
54
66
  end
55
67
 
68
+ # Notify the parent controller of an error condition.
69
+ # @parameters text [String] The details of the error condition.
70
+ # @parameters message [Hash] Additional details to send with the message.
56
71
  def error!(text, **message)
57
72
  send(status: text, **message)
58
73
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -28,39 +27,27 @@ require 'console/logger'
28
27
  module Async
29
28
  module Container
30
29
  module Notify
30
+ # Implements a general process readiness protocol with output to the local console.
31
31
  class Console < Client
32
+ # Open a notification client attached to the current console.
32
33
  def self.open!(logger = ::Console.logger)
33
34
  self.new(logger)
34
35
  end
35
36
 
37
+ # Initialize the notification client.
38
+ # @parameter logger [Console::Logger] The console logger instance to send messages to.
36
39
  def initialize(logger)
37
40
  @logger = logger
38
41
  end
39
42
 
43
+ # Send a message to the console.
40
44
  def send(level: :debug, **message)
41
45
  @logger.send(level, self) {message}
42
46
  end
43
47
 
44
- def ready!(**message)
45
- send(ready: true, **message)
46
- end
47
-
48
- def restarting!(**message)
49
- message[:ready] = false
50
- message[:reloading] = true
51
- message[:status] ||= "Restarting..."
52
-
53
- send(**message)
54
- end
55
-
56
- def reloading!(**message)
57
- message[:ready] = false
58
- message[:reloading] = true
59
- message[:status] ||= "Reloading..."
60
-
61
- send(**message)
62
- end
63
-
48
+ # Send an error message to the console.
49
+ # @parameters text [String] The details of the error condition.
50
+ # @parameters message [Hash] Additional details to send with the message.
64
51
  def error!(text, **message)
65
52
  send(status: text, level: :error, **message)
66
53
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -28,9 +27,12 @@ require 'json'
28
27
  module Async
29
28
  module Container
30
29
  module Notify
30
+ # Implements a process readiness protocol using an inherited pipe file descriptor.
31
31
  class Pipe < Client
32
+ # The environment variable key which contains the pipe file descriptor.
32
33
  NOTIFY_PIPE = 'NOTIFY_PIPE'
33
34
 
35
+ # Open a notification client attached to the current {NOTIFY_PIPE} if possible.
34
36
  def self.open!(environment = ENV)
35
37
  if descriptor = environment.delete(NOTIFY_PIPE)
36
38
  self.new(::IO.for_fd(descriptor.to_i))
@@ -41,6 +43,8 @@ module Async
41
43
  return nil
42
44
  end
43
45
 
46
+ # Initialize the notification client.
47
+ # @parameter io [IO] An IO instance used for sending messages.
44
48
  def initialize(io)
45
49
  @io = io
46
50
  end
@@ -72,6 +76,8 @@ module Async
72
76
  end
73
77
  end
74
78
 
79
+ # Formats the message using JSON and sends it to the parent controller.
80
+ # This is suitable for use with {Channel}.
75
81
  def send(**message)
76
82
  data = ::JSON.dump(message)
77
83
 
@@ -79,10 +85,6 @@ module Async
79
85
  @io.flush
80
86
  end
81
87
 
82
- def ready!(**message)
83
- send(ready: true, **message)
84
- end
85
-
86
88
  private
87
89
 
88
90
  def environment_for(arguments)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
4
  #
6
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -30,21 +29,31 @@ require 'kernel/sync'
30
29
  module Async
31
30
  module Container
32
31
  module Notify
32
+ # Implements the systemd NOTIFY_SOCKET process readiness protocol.
33
+ # See <https://www.freedesktop.org/software/systemd/man/sd_notify.html> for more details of the underlying protocol.
33
34
  class Socket < Client
35
+ # The name of the environment variable which contains the path to the notification socket.
34
36
  NOTIFY_SOCKET = 'NOTIFY_SOCKET'
37
+
38
+ # The maximum allowed size of the UDP message.
35
39
  MAXIMUM_MESSAGE_SIZE = 4096
36
40
 
41
+ # Open a notification client attached to the current {NOTIFY_SOCKET} if possible.
37
42
  def self.open!(environment = ENV)
38
43
  if path = environment.delete(NOTIFY_SOCKET)
39
44
  self.new(path)
40
45
  end
41
46
  end
42
47
 
48
+ # Initialize the notification client.
49
+ # @parameter path [String] The path to the UNIX socket used for sending messages to the process manager.
43
50
  def initialize(path)
44
51
  @path = path
45
52
  @endpoint = IO::Endpoint.unix(path, ::Socket::SOCK_DGRAM)
46
53
  end
47
54
 
55
+ # Dump a message in the format requied by `sd_notify`.
56
+ # @parameter message [Hash] Keys and values should be string convertible objects. Values which are `true`/`false` are converted to `1`/`0` respectively.
48
57
  def dump(message)
49
58
  buffer = String.new
50
59
 
@@ -62,6 +71,8 @@ module Async
62
71
  return buffer
63
72
  end
64
73
 
74
+ # Send the given message.
75
+ # @parameter message [Hash]
65
76
  def send(**message)
66
77
  data = dump(message)
67
78
 
@@ -76,6 +87,8 @@ module Async
76
87
  end
77
88
  end
78
89
 
90
+ # Send the specified error.
91
+ # `sd_notify` requires an `errno` key, which defaults to `-1` to indicate a generic error.
79
92
  def error!(text, **message)
80
93
  message[:errno] ||= -1
81
94
 
@@ -27,8 +27,12 @@ require_relative 'notify/pipe'
27
27
 
28
28
  module Async
29
29
  module Container
30
+ # Represents a running child process from the point of view of the parent container.
30
31
  class Process < Channel
32
+ # Represents a running child process from the point of view of the child process.
31
33
  class Instance < Notify::Pipe
34
+ # Wrap an instance around the {Process} instance from within the forked child.
35
+ # @parameter process [Process] The process intance to wrap.
32
36
  def self.for(process)
33
37
  instance = self.new(process.out)
34
38
 
@@ -46,16 +50,22 @@ module Async
46
50
  @name = nil
47
51
  end
48
52
 
53
+ # Set the process title to the specified value.
54
+ # @parameter value [String] The name of the process.
49
55
  def name= value
50
56
  if @name = value
51
57
  ::Process.setproctitle(@name)
52
58
  end
53
59
  end
54
60
 
61
+ # The name of the process.
62
+ # @returns [String]
55
63
  def name
56
64
  @name
57
65
  end
58
66
 
67
+ # Replace the current child process with a different one. Forwards arguments and options to {::Process.exec}.
68
+ # This method replaces the child process with the new executable, thus this method never returns.
59
69
  def exec(*arguments, ready: true, **options)
60
70
  if ready
61
71
  self.ready!(status: "(exec)") if ready
@@ -63,10 +73,13 @@ module Async
63
73
  self.before_spawn(arguments, options)
64
74
  end
65
75
 
76
+ # TODO prefer **options... but it doesn't support redirections on < 2.7
66
77
  ::Process.exec(*arguments, options)
67
78
  end
68
79
  end
69
80
 
81
+ # Fork a child process appropriate for a container.
82
+ # @returns [Process]
70
83
  def self.fork(**options)
71
84
  self.new(**options) do |process|
72
85
  ::Process.fork do
@@ -96,6 +109,8 @@ module Async
96
109
  # end
97
110
  # end
98
111
 
112
+ # Initialize the process.
113
+ # @parameter name [String] The name to use for the child process.
99
114
  def initialize(name: nil)
100
115
  super()
101
116
 
@@ -109,6 +124,8 @@ module Async
109
124
  self.close_write
110
125
  end
111
126
 
127
+ # Set the name of the process.
128
+ # Invokes {::Process.setproctitle} if invoked in the child process.
112
129
  def name= value
113
130
  @name = value
114
131
 
@@ -116,12 +133,17 @@ module Async
116
133
  ::Process.setproctitle(@name) if @pid.nil?
117
134
  end
118
135
 
136
+ # The name of the process.
137
+ # @attribute [String]
119
138
  attr :name
120
139
 
140
+ # A human readable representation of the process.
141
+ # @returns [String]
121
142
  def to_s
122
143
  "\#<#{self.class} #{@name}>"
123
144
  end
124
145
 
146
+ # Invoke {#terminate!} and then {#wait} for the child process to exit.
125
147
  def close
126
148
  self.terminate!
127
149
  self.wait
@@ -129,18 +151,22 @@ module Async
129
151
  super
130
152
  end
131
153
 
154
+ # Send `SIGINT` to the child process.
132
155
  def interrupt!
133
156
  unless @status
134
157
  ::Process.kill(:INT, @pid)
135
158
  end
136
159
  end
137
160
 
161
+ # Send `SIGTERM` to the child process.
138
162
  def terminate!
139
163
  unless @status
140
164
  ::Process.kill(:TERM, @pid)
141
165
  end
142
166
  end
143
167
 
168
+ # Wait for the child process to exit.
169
+ # @returns [::Process::Status] The process exit status.
144
170
  def wait
145
171
  if @pid && @status.nil?
146
172
  _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
@@ -24,6 +24,7 @@ require 'async/reactor'
24
24
 
25
25
  module Async
26
26
  module Container
27
+ # Tracks various statistics relating to child instances in a container.
27
28
  class Statistics
28
29
  def initialize
29
30
  @spawns = 0
@@ -31,26 +32,41 @@ module Async
31
32
  @failures = 0
32
33
  end
33
34
 
35
+ # How many child instances have been spawned.
36
+ # @attribute [Integer]
34
37
  attr :spawns
38
+
39
+ # How many child instances have been restarted.
40
+ # @attribute [Integer]
35
41
  attr :restarts
42
+
43
+ # How many child instances have failed.
44
+ # @attribute [Integer]
36
45
  attr :failures
37
46
 
47
+ # Increment the number of spawns by 1.
38
48
  def spawn!
39
49
  @spawns += 1
40
50
  end
41
51
 
52
+ # Increment the number of restarts by 1.
42
53
  def restart!
43
54
  @restarts += 1
44
55
  end
45
56
 
57
+ # Increment the number of failures by 1.
46
58
  def failure!
47
59
  @failures += 1
48
60
  end
49
61
 
62
+ # Whether there have been any failures.
63
+ # @returns [Boolean] If the failure count is greater than 0.
50
64
  def failed?
51
65
  @failures > 0
52
66
  end
53
67
 
68
+ # Append another statistics instance into this one.
69
+ # @parameter other [Statistics] The statistics to append.
54
70
  def << other
55
71
  @spawns += other.spawns
56
72
  @restarts += other.restarts
@@ -28,14 +28,22 @@ require 'async/logger'
28
28
 
29
29
  module Async
30
30
  module Container
31
+ # Represents a running child thread from the point of view of the parent container.
31
32
  class Thread < Channel
33
+ # Used to propagate the exit status of a child process invoked by {Instance#exec}.
32
34
  class Exit < Exception
35
+ # Initialize the exit status.
36
+ # @parameter status [::Process::Status] The process exit status.
33
37
  def initialize(status)
34
38
  @status = status
35
39
  end
36
40
 
41
+ # The process exit status.
42
+ # @attribute [::Process::Status]
37
43
  attr :status
38
44
 
45
+ # The process exit status if it was an error.
46
+ # @returns [::Process::Status | Nil]
39
47
  def error
40
48
  unless status.success?
41
49
  status
@@ -43,7 +51,10 @@ module Async
43
51
  end
44
52
  end
45
53
 
54
+ # Represents a running child thread from the point of view of the child thread.
46
55
  class Instance < Notify::Pipe
56
+ # Wrap an instance around the {Thread} instance from within the threaded child.
57
+ # @parameter thread [Thread] The thread intance to wrap.
47
58
  def self.for(thread)
48
59
  instance = self.new(thread.out)
49
60
 
@@ -57,14 +68,20 @@ module Async
57
68
  super
58
69
  end
59
70
 
71
+ # Set the name of the thread.
72
+ # @parameter value [String] The name to set.
60
73
  def name= value
61
74
  @thread.name = value
62
75
  end
63
76
 
77
+ # Get the name of the thread.
78
+ # @returns [String]
64
79
  def name
65
80
  @thread.name
66
81
  end
67
82
 
83
+ # Execute a child process using {::Process.spawn}. In order to simulate {::Process.exec}, an {Exit} instance is raised to propagage exit status.
84
+ # This creates the illusion that this method does not return (normally).
68
85
  def exec(*arguments, ready: true, **options)
69
86
  if ready
70
87
  self.ready!(status: "(spawn)") if ready
@@ -91,6 +108,8 @@ module Async
91
108
  end
92
109
  end
93
110
 
111
+ # Initialize the thread.
112
+ # @parameter name [String] The name to use for the child thread.
94
113
  def initialize(name: nil)
95
114
  super()
96
115
 
@@ -116,18 +135,25 @@ module Async
116
135
  end
117
136
  end
118
137
 
138
+ # Set the name of the thread.
139
+ # @parameter value [String] The name to set.
119
140
  def name= value
120
141
  @thread.name = value
121
142
  end
122
143
 
144
+ # Get the name of the thread.
145
+ # @returns [String]
123
146
  def name
124
147
  @thread.name
125
148
  end
126
149
 
150
+ # A human readable representation of the thread.
151
+ # @returns [String]
127
152
  def to_s
128
153
  "\#<#{self.class} #{@thread.name}>"
129
154
  end
130
155
 
156
+ # Invoke {#terminate!} and then {#wait} for the child thread to exit.
131
157
  def close
132
158
  self.terminate!
133
159
  self.wait
@@ -135,14 +161,18 @@ module Async
135
161
  super
136
162
  end
137
163
 
164
+ # Raise {Interrupt} in the child thread.
138
165
  def interrupt!
139
166
  @thread.raise(Interrupt)
140
167
  end
141
168
 
169
+ # Raise {Terminate} in the child thread.
142
170
  def terminate!
143
171
  @thread.raise(Terminate)
144
172
  end
145
173
 
174
+ # Wait for the thread to exit and return he exit status.
175
+ # @returns [Status]
146
176
  def wait
147
177
  if @waiter
148
178
  @waiter.join
@@ -152,15 +182,21 @@ module Async
152
182
  return @status
153
183
  end
154
184
 
185
+ # A pseudo exit-status wrapper.
155
186
  class Status
156
- def initialize(result = nil)
157
- @result = result
187
+ # Initialise the status.
188
+ # @parameter error [::Process::Status] The exit status of the child thread.
189
+ def initialize(error = nil)
190
+ @error = error
158
191
  end
159
192
 
193
+ # Whether the status represents a successful outcome.
194
+ # @returns [Boolean]
160
195
  def success?
161
- @result.nil?
196
+ @error.nil?
162
197
  end
163
198
 
199
+ # A human readable representation of the status.
164
200
  def to_s
165
201
  "\#<#{self.class} #{success? ? "success" : "failure"}>"
166
202
  end
@@ -168,6 +204,7 @@ module Async
168
204
 
169
205
  protected
170
206
 
207
+ # Invoked by the @waiter thread to indicate the outcome of the child thread.
171
208
  def finished(error = nil)
172
209
  if error
173
210
  Async.logger.error(self) {error}
@@ -24,13 +24,17 @@ require_relative 'generic'
24
24
  require_relative 'thread'
25
25
 
26
26
  module Async
27
- # Manages a reactor within one or more threads.
28
27
  module Container
28
+ # A multi-thread container which uses {Thread.fork}.
29
29
  class Threaded < Generic
30
+ # Indicates that this is not a multi-process container.
30
31
  def self.multiprocess?
31
32
  false
32
33
  end
33
34
 
35
+ # Start a named child thread 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
  Thread.fork(name: name, &block)
36
40
  end
@@ -22,6 +22,6 @@
22
22
 
23
23
  module Async
24
24
  module Container
25
- VERSION = "0.16.6"
25
+ VERSION = "0.16.7"
26
26
  end
27
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.6
4
+ version: 0.16.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-06 00:00:00.000000000 Z
11
+ date: 2020-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -161,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - "~>"
163
163
  - !ruby/object:Gem::Version
164
- version: '2.0'
164
+ version: '2.5'
165
165
  required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - ">="