Fun with Ruby: IT'S A TRAP!

Ryan Briones, May 23, 2011

Recently on a project I was tasked with creating an extremely long-running task that is intensive in unknown ways on our database and not idempotent. This meant unforeseen performance issues could cause us to have to kill our long-running task and leave us in a state that was painful to recover from.

To alleviate some pain I decided that I would trap the kill signal. If you pressed Ctl-C once, it would output a message “Press Ctl-C one more time to Kill now. Otherwise, the process will terminate at the next round.” This idea I stole from Ruby projects like autotest and mongrel. In autotest, pressing Ctl-C tells autotest to run all tests manually whereas pressing Ctl-C twice means quit autotest. In mongrel, Ctl-C starts the shutdown process, but waits for each child to stop it’s request before exiting. Ctl-C again tells mongrel to abort and exit immediately.

This pattern can be extremely powerful and is super easy to implement in Ruby. For instance, let’s say I want to output a message when exiting due to Ctl-C, here’s what that code might look like:

trap("INT") do
  puts "Whoa there big dogg. I'll exit in a second!"
  sleep 1
  exit
end

while true
  # infinite loop to simulate "long running process"
end

Here we’ve told Ruby to capture the INT signal, which is POSIX signal that is emitted when you press Ctl-C. In this particular code, we’ve decided to be nice and exit when our user sends our program SIGINT. A funner version of this program could be this:

trap("INT") do
  puts "I'mma let you finish, but SIGKILL is the best POSIX signal of all time"
end

while true
  # infinite loop to simulate "long running process"
end

This is probably not useful though. How about a more practical version:

$shutdown = false

trap("INT") do
  if $shutdown
    puts "Prematurely exiting"
    exit
  end

  puts "Starting shutdown procedure"
  $shutdown = true
end

while true
  # infinite loop to simulate "long running process"
  if $shutdown
    sleep 10 # simulate shutdown procedure
    break
  end
end

Here we setup some global state for our process in $shutdown. It tells our event loop when to start the shutdown procedure. When the shutdown procedure is finished, the event loop will exit and the process will terminate. We can also terminate the program by sending SIGINT twice. This is the case where we need to subvert the shutdown procedure and just shutdown.

A practical example of using signal trapping is in my cgiup gem; a gem I wrote for running a single “CGI script” as an application mounted at http://localhost:2000. Here is the juicy stuff:

#!/usr/bin/env ruby
# From: https://github.com/ryanbriones/cgiup/blob/master/bin/cgiup
require 'webrick'

unless ARGV[0]
  STDERR.puts "usage: cgiup [PATH TO CGI SCRIPT]"
  exit 1
end

cgi_script = File.expand_path(ARGV[0])

unless File.exists?(cgi_script)
  STDERR.puts "CGI Script: #{cgi_script} Not Found"
  exit 1
end

server = WEBrick::HTTPServer.new(:Port => 2000)
server.mount('/', WEBrick::HTTPServlet::CGIHandler, File.expand_path(ARGV[0]))

trap("INT"){ server.shutdown }
server.start

Here we have the same basic setup as in our examples above except that we’re not tracking state in a global variable. Instead we have an object, our WEBrick::HTTPServer instance, that just gets a method call to #shutdown when SIGINT is received. After we setup that trap, the server’s event loop is started and runs. This is the more common use case for signal trapping.

There are other things you could do as well. For instance in the autotest example there is a timeout of a few seconds or so when pressing Ctl-C before all tests are run. If you send SIGINT again during that time period, autotest will stop instead. Also, trap isn’t only for SIGINT. You can capture any POSIX Signal with trap. For instance, many daemonized servers implement SIGHUP for log rolling or configuration reloading. The possibilities are ENDLESS… within the list of POSIX signals your operating system implements. See the linked Wikipedia page for a list of well known POSIX Signals and their traditional uses.

Well, that’s the basics of signal trapping. It’s another tool in your box for great good and great fun!

Rss-icon Rss-icon-over
Archive

Archive