class Async::Container::Controller

Manages the life-cycle of one or more containers in order to support a persistent system. e.g. a web server, job server or some other long running system.

Constants

SIGHUP
SIGINT
SIGTERM
SIGUSR1
SIGUSR2

Attributes

container[R]

The current container being managed by the controller.

Public Class Methods

new(notify: Notify.open!) click to toggle source

Initialize the controller. @parameter notify [Notify::Client] A client used for process readiness notifications.

# File lib/async/container/controller.rb, line 42
def initialize(notify: Notify.open!)
        @container = nil
        
        if @notify = notify
                @notify.status!("Initializing...")
        end
        
        @signals = {}
        
        trap(SIGHUP) do
                self.restart
        end
end

Public Instance Methods

create_container() click to toggle source

Create a container for the controller. Can be overridden by a sub-class. @returns [Generic] A specific container instance to use.

# File lib/async/container/controller.rb, line 85
def create_container
        Container.new
end
reload() click to toggle source

Reload the existing container. Children instances will be reloaded using `SIGHUP`.

# File lib/async/container/controller.rb, line 168
def reload
        @notify&.reloading!
        
        Console.logger.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.logger.debug(self, "Waiting for startup...")
        @container.wait_until_ready
        Console.logger.debug(self, "Finished startup.")
        
        if @container.failed?
                @notify.error!("Container failed!")
                
                raise SetupError, @container
        else
                @notify&.ready!
        end
end
restart() click to toggle source

Restart the container. A new container is created, and if successful, any old container is terminated gracefully.

# File lib/async/container/controller.rb, line 121
def restart
        if @container
                @notify&.restarting!
                
                Console.logger.debug(self) {"Restarting container..."}
        else
                Console.logger.debug(self) {"Starting container..."}
        end
        
        container = self.create_container
        
        begin
                self.setup(container)
        rescue
                @notify&.error!($!.to_s)
                
                raise SetupError, container
        end
        
        # Wait for all child processes to enter the ready state.
        Console.logger.debug(self, "Waiting for startup...")
        container.wait_until_ready
        Console.logger.debug(self, "Finished startup.")
        
        if container.failed?
                @notify&.error!($!.to_s)
                
                container.stop
                
                raise SetupError, container
        end
        
        # Make this swap as atomic as possible:
        old_container = @container
        @container = container
        
        Console.logger.debug(self, "Stopping old container...")
        old_container&.stop
        @notify&.ready!
rescue
        # If we are leaving this function with an exception, try to kill the container:
        container&.stop(false)
        
        raise
end
run() click to toggle source

Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.

# File lib/async/container/controller.rb, line 194
def run
        # 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
        
        hangup_action = Signal.trap(:HUP) do
                raise Hangup
        end
        
        self.start
        
        while @container&.running?
                begin
                        @container.wait
                rescue SignalException => exception
                        if handler = @signals[exception.signo]
                                begin
                                        handler.call
                                rescue SetupError => error
                                        Console.logger.error(self) {error}
                                end
                        else
                                raise
                        end
                end
        end
rescue Interrupt
        self.stop(true)
rescue Terminate
        self.stop(false)
ensure
        self.stop(true)
        
        # Restore the interrupt handler:
        Signal.trap(:INT, interrupt_action)
        Signal.trap(:TERM, terminate_action)
        Signal.trap(:HUP, hangup_action)
end
running?() click to toggle source

Whether the controller has a running container. @returns [Boolean]

# File lib/async/container/controller.rb, line 91
def running?
        !!@container
end
setup(container) click to toggle source

Spawn container instances into the given container. Should be overridden by a sub-class. @parameter container [Generic] The container, generally from {#create_container}.

# File lib/async/container/controller.rb, line 103
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
start() click to toggle source

Start the container unless it's already running.

# File lib/async/container/controller.rb, line 109
def start
        self.restart unless @container
end
state_string() click to toggle source

The state of the controller. @returns [String]

# File lib/async/container/controller.rb, line 58
def state_string
        if running?
                "running"
        else
                "stopped"
        end
end
stop(graceful = true) click to toggle source

Stop the container if it's running. @parameter graceful [Boolean] Whether to give the children instances time to shut down or to kill them immediately.

# File lib/async/container/controller.rb, line 115
def stop(graceful = true)
        @container&.stop(graceful)
        @container = nil
end
to_s() click to toggle source

A human readable representation of the controller. @returns [String]

# File lib/async/container/controller.rb, line 68
def to_s
        "#{self.class} #{state_string}"
end
trap(signal, &block) click to toggle source

Trap the specified signal. @parameters signal [Symbol] The signal to trap, e.g. `:INT`. @parameters block [Proc] The signal handler to invoke.

# File lib/async/container/controller.rb, line 75
def trap(signal, &block)
        @signals[signal] = block
end
wait() click to toggle source

Wait for the underlying container to start.

# File lib/async/container/controller.rb, line 96
def wait
        @container&.wait
end