class 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.
Definitions
def initialize(notify: Notify.open!, container_class: Container, graceful_stop: true)
Initialize the controller.
Signature
-
parameter
notify
Notify::Client
A client used for process readiness notifications.
Implementation
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 state_string
The state of the controller.
Signature
-
returns
String
Implementation
def state_string
if running?
"running"
else
"stopped"
end
end
def to_s
A human readable representation of the controller.
Signature
-
returns
String
Implementation
def to_s
"#{self.class} #{state_string}"
end
def trap(signal, &block)
Trap the specified signal.
Implementation
def trap(signal, &block)
@signals[signal] = block
end
attr :container
The current container being managed by the controller.
def create_container
Create a container for the controller. Can be overridden by a sub-class.
Signature
-
returns
Generic
A specific container instance to use.
Implementation
def create_container
@container_class.new
end
def running?
Whether the controller has a running container.
Signature
-
returns
Boolean
Implementation
def running?
!!@container
end
def wait
Wait for the underlying container to start.
Implementation
def wait
@container&.wait
end
def setup(container)
Spawn container instances into the given container. Should be overridden by a sub-class.
Signature
-
parameter
container
Generic
The container, generally from
#create_container
.
Implementation
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
Start the container unless it's already running.
Implementation
def start
unless @container
Console.info(self) {"Controller starting..."}
self.restart
end
Console.info(self) {"Controller started..."}
end
def stop(graceful = @graceful_stop)
Stop the container if it's running.
Signature
-
parameter
graceful
Boolean
Whether to give the children instances time to shut down or to kill them immediately.
Implementation
def stop(graceful = @graceful_stop)
@container&.stop(graceful)
@container = nil
end
def restart
Restart the container. A new container is created, and if successful, any old container is terminated gracefully. This is equivalent to a blue-green deployment.
Implementation
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!
ensure
# If we are leaving this function with an exception, try to kill the container:
container&.stop(false)
end
def reload
Reload the existing container. Children instances will be reloaded using SIGHUP
.
Implementation
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 run
Enter the controller run loop, trapping SIGINT
and SIGTERM
.
Implementation
def run
@notify&.status!("Initializing...")
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