lib/async/container/controller.rb in async-container-0.15.0 vs lib/async/container/controller.rb in async-container-0.16.0

- old
+ new

@@ -20,14 +20,15 @@ require_relative 'error' require_relative 'best' require_relative 'statistics' +require_relative 'notify' module Async module Container - class ContainerFailed < Error + class ContainerError < Error def initialize(container) super("Could not create container!") @container = container end @@ -35,85 +36,175 @@ end # Manages the life-cycle of a container. class Controller SIGHUP = Signal.list["HUP"] - DEFAULT_TIMEOUT = 2 + SIGINT = Signal.list["INT"] + SIGTERM = Signal.list["TERM"] + SIGUSR1 = Signal.list["USR1"] + SIGUSR2 = Signal.list["USR2"] - def initialize(startup_duration: DEFAULT_TIMEOUT) + def initialize(notify: Notify.open!) @container = nil - @startup_duration = startup_duration + if @notify = notify + @notify.status!("Initializing...") + end + + @signals = {} + + trap(SIGHUP, &self.method(:restart)) end + def state_string + if running? + "running" + else + "stopped" + end + end + + def to_s + "#{self.class} #{state_string}" + end + + def trap(signal, &block) + @signals[signal] = block + end + attr :container def create_container Container.new end + def running? + !!@container + end + + def wait + @container&.wait + end + 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 - self.restart + self.restart unless @container end def stop(graceful = true) @container&.stop(graceful) @container = nil end - def restart(duration = @startup_duration) - hup_action = Signal.trap(:HUP, :IGNORE) + def restart + if @container + @notify&.restarting! + + Async.logger.debug(self) {"Restarting container..."} + else + Async.logger.debug(self) {"Starting container..."} + end + container = self.create_container begin self.setup(container) rescue - raise ContainerFailed, container + @notify&.error!($!.to_s) + + raise ContainerError, container end + # Wait for all child processes to enter the ready state. Async.logger.debug(self, "Waiting for startup...") - container.sleep(duration) + container.wait_until_ready Async.logger.debug(self, "Finished startup.") if container.failed? + @notify&.error!($!.to_s) + container.stop - raise ContainerFailed, container + raise ContainerError, container end - @container&.stop + # Make this swap as atomic as possible: + old_container = @container @container = container - ensure - Signal.trap(:HUP, hup_action) + + old_container&.stop + @notify&.ready! + rescue + # If we are leaving this function with an exception, try to kill the container: + container&.stop(false) end + def reload + @notify&.reloading! + + Async.logger.info(self) {"Reloading container: #{@container}..."} + + begin + self.setup(@container) + rescue + raise ContainerError, container + end + + # Wait for all child processes to enter the ready state. + Async.logger.debug(self, "Waiting for startup...") + @container.wait_until_ready + Async.logger.debug(self, "Finished startup.") + + if @container.failed? + @notify.error!("Container failed!") + + raise ContainerError, @container + else + @notify&.ready! + end + end + def run - Async.logger.debug(self) {"Starting container..."} + # I thought this was the default... but it doesn't always raise an exception unless you do this explicitly. + interrupt_action = Signal.trap(:INT) do + raise Interrupt + end + terminate_action = Signal.trap(:TERM) do + raise Terminate + end + self.start - while true + while @container&.running? begin @container.wait rescue SignalException => exception - if exception.signo == SIGHUP - Async.logger.info(self) {"Reloading container..."} - + if handler = @signals[exception.signo] begin - self.restart - rescue ContainerFailed => failure + handler.call + rescue ContainerError => failure Async.logger.error(self) {failure} end else raise end end end + rescue Interrupt + self.stop(true) + rescue Terminate + self.stop(false) + else + self.stop(true) ensure - self.stop + # Restore the interrupt handler: + Signal.trap(:INT, interrupt_action) + Signal.trap(:TERM, terminate_action) end end end end