Async::ContainerSourceAsyncContainerGeneric

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 count Integer

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 policy Policy

The policy to use for managing child lifecycle events.

parameter options Hash

Options passed to the class Async::Container::Group instance.

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 duration Numeric

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 timeout Boolean | 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 name String

The name of the child instance.

parameter restart Boolean

Whether to restart the child instance if it fails.

parameter key Symbol

An optional key used to look up (via Async::Container::Generic#[]) and reuse the child instance.

parameter health_check_timeout Numeric | Nil

The maximum time a child instance can run without updating its state, before it is terminated as unhealthy.

parameter startup_timeout Numeric | 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#spawn or Async::Container::Generic.run instead.

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