This is part two in a series of posts about signal handling in Ruby.
In the last post we looked at the two mechanisms Ruby provides for handling signals. Now we will combine them into a utility for gracefully handling interrupts and providing a way to safely shutdown our process.
Here is our proposed usage, we start with a worker that can handle a Shutdown
exception, and returns an unfinished jobs to the queue before shutting down.
class Worker < Struct.new(:jobs_queue)
def work
job = jobs_queue.pop
# work...
rescue Shutdown => shutdown
jobs_queue.push job if job.incomplete?
shutdown.continue
end
end
Then we will run our worker with graceful handling of shutdowns:
GracefulShutdown.new.handle_signals do
jobs = Queue.new
Worker.new(jobs).start
end
We will work our way to the full implementation starting with setting up signal
traps and raising the Shutdown
exception. We also want it to restore existing
handlers if no signal is received.
Shutdown = Class.new(RuntimeError)
class GracefulShutdown
DEFAULT_SIGNALS = ['INT', 'TERM']
def handle_signals(*signals)
signals = DEFAULT_SIGNALS if signals.empty?
handlers = setup(signals)
yield if block_given?
teardown(handlers)
rescue Shutdown
exit
end
private
def setup(signals)
signals.each_with_object({}) do |signal, handlers|
handlers[signal] = trap(signal) do
raise Shutdown
end
end
end
def teardown(handlers)
handlers.each do |signal, handler|
trap(signal, handler)
end
end
end
Now the worker can intercept the Shutdown
exception, but it’s must re-raise
it, which is a bit unobvious. There’s no good mechanism for ensuring the
exception is re-raised , but we can add some clarity with the continue
method
that facilitates this. We’ll also throw in an ignore
, just for extra clarity.
class Shutdown < RuntimeError
def continue
raise self
end
def ignore
# No-op, provided for clarity.
end
end
Now we can finish up with by briefly adding our higher helper method:
def WithGracefulShutdown(*signals, &block)
GracefulShutdown.new.handle_signals(*signals, &block)
end
There are plenty of improvements that we could make: The Shutdown
exception
can also provide an abort
method, which tears down the handlers and
communicates where the shutdown was aborted from. The GracefulShutdown
handler
could also provide callbacks for logging and a way to force shutdown if the same
signal is received twice.
In the next post we’ll look at some of these improvements and how to test signal
handling using RSpec. I’ve also packaged up the code from this post into a gem,
appropriately named graceful_shutdown
. You can sneak a peak at how it’s
tested if you look at the source on GitHub.