class Async::Container::Controller
e.g. a web server, job server or some other long running system.
Manages the life-cycle of one or more containers in order to support a persistent system.
def create_container
Can be overridden by a sub-class.
Create a container for the controller.
def create_container @container_class.new end
def initialize(notify: Notify.open!, container_class: Container, graceful_stop: true)
Initialize the controller.
def initialize(notify: Notify.open!, container_class: Container, graceful_stop: true) @container = nil @container_class = container_class @notify = notify @signals = {} self.trap(SIGHUP) do self.restart end @graceful_stop = graceful_stop end
def reload
def reload @notify&.reloading! Console.info(self) {"Reloading container: #{@container}..."} begin self.setup(@container) rescue raise SetupError, container end # Wait for all child processes to enter the ready state. Console.debug(self, "Waiting for startup...") @container.wait_until_ready Console.debug(self, "Finished startup.") if @container.failed? @notify.error!("Container failed to reload!") raise SetupError, @container else @notify&.ready! end end
def restart
Restart the container. A new container is created, and if successful, any old container is terminated gracefully.
def restart if @container @notify&.restarting! Console.debug(self) {"Restarting container..."} else Console.debug(self) {"Starting container..."} end container = self.create_container begin self.setup(container) rescue => error @notify&.error!(error.to_s) raise SetupError, container end # Wait for all child processes to enter the ready state. Console.debug(self, "Waiting for startup...") container.wait_until_ready Console.debug(self, "Finished startup.") if container.failed? @notify&.error!("Container failed to start!") container.stop(false) raise SetupError, container end # The following swap should be atomic: old_container = @container @container = container container = nil if old_container Console.debug(self, "Stopping old container...") old_container&.stop(@graceful_stop) end @notify&.ready!(size: @container.size) ensure # If we are leaving this function with an exception, try to kill the container: container&.stop(false) end
def run
def run @notify&.status!("Initializing controller...") with_signal_handlers do self.start while @container&.running? begin @container.wait rescue SignalException => exception if handler = @signals[exception.signo] begin handler.call rescue SetupError => error Console.error(self, error) end else raise end end end end rescue Interrupt self.stop rescue Terminate self.stop(false) ensure self.stop(false) end
def running?
Whether the controller has a running container.
def running? !!@container end
def setup(container)
Should be overridden by a sub-class.
Spawn container instances into the given container.
def setup(container) # Don't do this, otherwise calling super is risky for sub-classes: # raise NotImplementedError, "Container setup is must be implemented in derived class!" end
def start
def start unless @container Console.info(self) {"Controller starting..."} self.restart end Console.info(self) {"Controller started..."} end
def state_string
The state of the controller.
def state_string if running? "running" else "stopped" end end
def stop(graceful = @graceful_stop)
Stop the container if it's running.
def stop(graceful = @graceful_stop) @container&.stop(graceful) @container = nil end
def to_s
A human readable representation of the controller.
def to_s "#{self.class} #{state_string}" end
def trap(signal, &block)
@parameters signal [Symbol] The signal to trap, e.g. `:INT`.
Trap the specified signal.
def trap(signal, &block) @signals[signal] = block end
def wait
def wait @container&.wait end
def with_signal_handlers
def with_signal_handlers ught this was the default... but it doesn't always raise an exception unless you do this explicitly. pt_action = Signal.trap(:INT) do se `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly. err.puts "Received INT signal, interrupting...", caller ad.current.raise(Interrupt) te_action = Signal.trap(:TERM) do err.puts "Received TERM signal, terminating...", caller ad.current.raise(Terminate) action = Signal.trap(:HUP) do err.puts "Received HUP signal, restarting...", caller ad.current.raise(Restart) d.handle_interrupt(SignalException => :never) do re the interrupt handler: trap(:INT, interrupt_action) trap(:TERM, terminate_action) trap(:HUP, hangup_action)