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.