class Generic
A base class for implementing containers.
Definitions
def self.run(...)
Run a new container.
Implementation
def self.run(...)
self.new.run(...)
end
def run(count: Container.processor_count, **options, &block)
Run multiple instances of the same block in the container.
Signature
-
parameter
countInteger The number of instances to start.
Implementation
def run(count: Container.processor_count, **options, &block)
count.times do
spawn(**options, &block)
end
return self
end
def initialize(policy: Policy::DEFAULT, **options)
Initialize the container.
Signature
-
parameter
policyPolicy The policy to use for managing child lifecycle events.
-
parameter
optionsHash Options passed to the
class Async::Container::Groupinstance.
Implementation
def initialize(policy: Policy::DEFAULT, **options)
@group = Group.new(**options)
@stopping = false
@state = {}
@policy = policy
@statistics = @policy.make_statistics
@keyed = {}
end
attr :group
Signature
-
attribute
Group The group of running children instances.
def size
Signature
-
returns
Integer The number of running children instances.
Implementation
def size
@group.size
end
attr :state
Signature
-
attribute
Hash(Child, Hash) The state of each child instance.
attr_accessor :policy
Signature
-
attribute
Policy The policy for managing child lifecycle events.
def to_s
A human readable representation of the container.
Signature
-
returns
String
Implementation
def to_s
"#{self.class} with #{@statistics.spawns} spawns and #{@statistics.failures} failures."
end
def [](key)
Look up a child process by key. A key could be a symbol, a file path, or something else which the child instance represents.
Implementation
def [] key
@keyed[key]
end
attr :statistics
Statistics relating to the behavior of children instances.
Signature
-
attribute
Statistics
def failed?
Whether any failures have occurred within the container.
Signature
-
returns
Boolean
Implementation
def failed?
@statistics.failed?
end
def running?
Whether the container has running children instances.
Implementation
def running?
@group.running?
end
def stopping?
Whether the container is currently stopping.
Signature
-
returns
Boolean
Implementation
def stopping?
@stopping
end
def sleep(duration = nil)
Sleep until some state change occurs or the specified duration elapses.
Signature
-
parameter
durationNumeric the maximum amount of time to sleep for.
Implementation
def sleep(duration = nil)
@group.sleep(duration)
end
def wait
Wait until all spawned tasks are completed.
Implementation
def wait
@group.wait
end
def interrupt
Gracefully interrupt all child instances.
Implementation
def interrupt
# We must enter the stopping state before signalling the children. Interrupting a child causes it to drain and exit, but the main run loop will respawn any child that exits while `restart: true` and the container is not stopping (see the `restart && !@stopping` gate in `#run`). Without setting this flag, an interrupted child immediately respawns, so the container never drains and `#wait` never returns.
#
# This matters most for `Hybrid` containers: a `SIGINT`/`SIGTERM` delivered to a fork is translated into a call to `#interrupt` on the inner threaded container, which typically runs with `restart: true` (the default for `async-service` managed services). If `#interrupt` did not set this flag, the inner threads would drain, exit, and respawn in a loop, so a single signal would never terminate the fork. Setting `@stopping = true` here makes `#interrupt` behave as the start of a graceful shutdown: children drain and exit, are not respawned, and the fork terminates - consistent with how `Forked` and `Threaded` containers handle a single interrupt.
@stopping = true
@group.interrupt
end
def status?(flag)
Returns true if all children instances have the specified status flag set.
e.g. :ready.
This state is updated by the process readiness protocol mechanism. See class Async::Container::Notify::Client for more details.
Signature
-
returns
Boolean
Implementation
def status?(flag)
# This also returns true if all processes have exited/failed:
@state.all?{|_, state| state[flag]}
end
def wait_until_ready
Wait until all the children instances have indicated that they are ready.
Signature
-
returns
Boolean The children all became ready.
Implementation
def wait_until_ready
while true
Console.debug(self) do |buffer|
buffer.puts "Waiting for ready:"
@state.each do |child, state|
buffer.puts "\t#{child.inspect}: #{state}"
end
end
self.sleep
if self.status?(:ready)
Console.debug(self) do |buffer|
buffer.puts "All ready:"
@state.each do |child, state|
buffer.puts "\t#{child.inspect}: #{state}"
end
end
return true
end
end
end
def stop(timeout = true)
Stop the children instances.
Signature
-
parameter
timeoutBoolean | Numeric Whether to stop gracefully, or a specific timeout.
Implementation
def stop(timeout = true)
if @stopping
Console.warn(self, "Container is already stopping!")
return
end
Console.info(self, "Stopping container...", timeout: timeout)
@stopping = true
@group.stop(timeout)
if @group.running?
Console.warn(self, "Group is still running after stopping it!")
else
Console.info(self, "Group has stopped.")
end
rescue => error
Console.error(self, "Error while stopping container!", exception: error)
raise
end
def spawn(name: nil, restart: false, key: nil, health_check_timeout: nil, startup_timeout: nil, &block)
Spawn a child instance into the container.
Signature
-
parameter
nameString The name of the child instance.
-
parameter
restartBoolean Whether to restart the child instance if it fails.
-
parameter
keySymbol An optional key used to look up (via
Async::Container::Generic#[]) and reuse the child instance.-
parameter
health_check_timeoutNumeric | Nil The maximum time a child instance can run without updating its state, before it is terminated as unhealthy.
-
parameter
startup_timeoutNumeric | Nil The maximum time a child instance can run without becoming ready, before it is terminated as unhealthy.
Implementation
def spawn(name: nil, restart: false, key: nil, health_check_timeout: nil, startup_timeout: nil, &block)
name ||= UNNAMED
if reuse?(key)
Console.debug(self, "Reusing existing child.", child: {key: key, name: name})
return false
end
@statistics.spawn!
fiber do
until @stopping
Console.debug(self, "Starting child...", child: {key: key, name: name, restart: restart, health_check_timeout: health_check_timeout}, statistics: @statistics)
child = self.start(name, &block)
state = insert(key, child)
# Notify policy of spawn
begin
@policy.child_spawn(self, child, name: name, key: key)
rescue => error
Console.error(self, "Policy error in child_spawn!", exception: error)
end
Console.debug(self, "Started child.", child: child, spawn: {key: key, restart: restart, health_check_timeout: health_check_timeout}, statistics: @statistics)
# If a health check or startup timeout is specified, we will monitor the child process and terminate it if it does not update its state within the specified time.
if health_check_timeout || startup_timeout
age_clock = state[:age] = Clock.start
end
status = nil
begin
status = @group.wait_for(child) do |message|
case message
when :health_check!
if state[:ready]
# If a health check timeout is specified, we will monitor the child process and terminate it if it does not update its state within the specified time.
if health_check_timeout
if health_check_timeout < age_clock.total
health_check_failed(child, age_clock, health_check_timeout)
end
end
else
# If a startup timeout is specified, we will monitor the child process and terminate it if it does not become ready within the specified time.
if startup_timeout
if startup_timeout < age_clock.total
startup_failed(child, age_clock, startup_timeout)
end
end
end
else
state.update(message)
# Reset the age clock if the child has become ready:
if state[:ready]
age_clock&.reset!
end
end
end
rescue => error
Console.error(self, "Error during child process management!", exception: error, stopping: @stopping)
ensure
delete(key, child)
end
if status&.success?
Console.debug(self, "Child exited successfully.", status: status, stopping: @stopping)
else
@statistics.failure!
Console.error(self, "Child exited with error!", status: status, stopping: @stopping)
end
# Notify policy of exit (after statistics are updated):
begin
@policy.child_exit(self, child, status, name: name, key: key)
rescue => error
Console.error(self, "Policy error in child_exit!", exception: error)
end
if restart && !@stopping
@statistics.restart!
else
break
end
end
end.resume
return true
end
def async(**options, &block)
- deprecated
Signature
- deprecated
Please use
Async::Container::Generic#spawnorAsync::Container::Generic.runinstead.
Implementation
def async(**options, &block)
# warn "#{self.class}##{__method__} is deprecated, please use `spawn` or `run` instead.", uplevel: 1
require "async"
spawn(**options) do |instance|
Async(instance, &block)
end
end
def reload
Re-run the given block against the container.
Existing keyed children are reused (see Async::Container::Generic#spawn), so re-running setup will not
duplicate them. Reconciliation of children whose keys are no longer configured
(i.e. stopping obsolete children) is not currently supported and will be revisited.
Implementation
def reload
yield
end
def reuse?(key)
Whether a child instance already exists for the given key, in which case it can be reused rather than spawned again.
Implementation
def reuse?(key)
if key
@keyed.key?(key)
else
false
end
end
def key?(key)
Whether a child instance exists for the given key.
Implementation
def key?(key)
if key
@keyed.key?(key)
end
end
def insert(key, child)
Register the child (value) as running.
Implementation
def insert(key, child)
if key
@keyed[key] = child
end
state = {}
@state[child] = state
return state
end
def delete(key, child)
Clear the child (value) as running.
Implementation
def delete(key, child)
if key
@keyed.delete(key)
end
@state.delete(child)
end