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