This is part one in a series of posts about signal handling in Ruby.
Ruby programs can handle signals in two ways. The most common is with the trap
command. This lets you attach a block of code to execute when the signal is
received.
trap('INT') do
puts 'Exiting...'
exit
end
loop do
work
end
The second way is to rescue a SignalException
, which also has Interrupt
as a
subclass.
loop do
begin
work
rescue Interrupt
puts 'Exiting...'
break
end
end
Unlike trap
, you can rescue a signal from multiple places in your program. So
long as you re-raise the signal after handling it, each handler within the
callstack will be invoked.
So what is the difference and which mechanism is better? It’s helpful to think of them as complimentary, letting you deal with signals from two sides of your program.
The trap
method defines a handler at the ‘top’ of your program, in other
words, it deals with the signal when it is first received. If the signal is not
trapped, the default behaviour is to raise an exception.
It works very much like a callback, with some caveats. There can only be one handler per signal for a Ruby process; so if another trap is set for the same signal, your handler may never be triggered.
trap('INT') { puts 'foo'; exit }
trap('INT') { puts 'bar'; exit }
By contract, the rescue SignalException
method defines the handler at the
‘bottom’ of your program. Because the default response to an untrapped signal is
to raise a SignalException
, the signal can work its way backwards through the
callstack until something rescues it. When it reaches the top, the program will
exit, albeit a little ungracefully.
Now that we understand this, we can make appropriate use of these methods. If we
just need our program to shut down gracefully with an appropriate signal is
received, we would use trap
.
trap('INT') do
exit
end
STDIN.each_line do |line|
puts line.reverse
end
For larger apps, it might not be practical, or appropriate to setup a signal
trap, in which case the rescue
method is better suited. Here we have a Rails
script that is processing jobs from a queue. If our program is interrupted, we
want to prevent dropping the job, so it is put back on the queue before exiting.
job_queue.subscribe do |queue, job|
begin
job.perform
rescue Interrupt
queue.push(job)
exit
end
end
Next time we’ll look at a few ways to combine these two approaches into a system for handling graceful shutdowns, shutting down only for repeat signals, and making defining multiple traps on the same signal.