Skip to content

Instantly share code, notes, and snippets.

@kylev
Last active August 29, 2015 14:26
Show Gist options
  • Save kylev/68f69ecfed9f48fc055b to your computer and use it in GitHub Desktop.
Save kylev/68f69ecfed9f48fc055b to your computer and use it in GitHub Desktop.
An exploration of common pitfalls in Ruby Exception/Signal handling.
#!/usr/bin/env ruby
# Some members of ruby community continue to misunderstand the interplay of
# signal handlers and exception handling. This is a brief exploration
# via examples. You can run them by uncommenting the invocations at
# the bottom. You can see their behavior with Ctrl-C.
#
# Some of the examples will require a kill -9 from another terminal to
# stop.
#
# The core lessons here are (1) keep signal handlers short and (2)
# catching Exception is wrong 99% of the time (you should usually
# catch StandardError or more specific subclasses).
# This is a fake work unit. Right now it sleeps, but you might
# consider doing some math, database lookups, queued work handling, or
# network chatter. Sometimes you might even throw exceptions from
# here.
#
# Try raising something mundane like an IndexError or IOError.
def do_work
sleep(1)
printf('.')
end
# This is our baseline that we're all familiar with. If you hit Ctrl-C,
# the process will stop suddenly, no matter what it was doing.
def plain_jane
loop do
do_work
end
puts "Most coders know they won't get here on Ctrl-C."
end
# This is how you anger your DevOps team. Without any signal handlers
# installed, pressing Ctrl-C or doing a regular `kill` will throw an
# Interrupt or SignalException. The only way out is `kill -9`. You
# don't want to train your DevOps team to kill your code with a sledge
# hammer, do you?
#
# This is the base case where most people will learn why we say not to
# catch Exception. But they'll stumble on this while trying to implement
# "This daemon shouldn't crash."
def sysadmin_will_hate_you
loop do
begin
do_work
rescue Exception => e
puts "I grabbed #{e.class.name}"
end
end
puts "After loop can't be reach because kill -9 is brutal."
end
# This is more subtle, but a pretty common first (bad) attempt at
# signal handling. You might expect calling exit(1) to kill the
# process no matter what. However, since Ruby internally implements
# exit() by throwing SystemExit, you can break its behavior with bad
# exception handling (again with catching Exception).
#
# On a side note, I consider calling exit() from a signal handler to
# be a code smell. If you're exiting from a signal handler, there's
# really no way that your cleanup code elsewhere could possibly run.
def hard_exit_fail
Signal.trap('INT') do |signo|
# Ctrl-C will reach here.
puts "Handler was called with #{signo}."
exit(1)
end
loop do
begin
do_work
rescue Exception => e
puts "LOL, I grabbed #{e.class.name}, no exit for you!"
end
end
puts 'Again, no after loop code will run since kill -9 is the escape.'
end
# This is exactly like `hard_exit_fail` but catches the proper
# StandardError (as ruby does with bare "rescue"). This means that the
# signal handler that wishes to cause an immediate shutdown actually
# works.
#
# Still, none of the after-loop code (which might do cleanup) will be
# executed. That's why I think exit() in a signal handler is a code
# smell.
def hard_exit_semi_success
Signal.trap('INT') do |signo|
puts "Handler was called with #{signo}."
exit(1)
end
loop do
begin
do_work
rescue StandardError => e
puts 'SystemExit will fly past me.'
end
end
puts 'Again, no after code gets run, no cleanup possible.'
end
# This is the more canonical approach to a long-running
# process. You've probably got some sort of central loop that you can
# check for the shutdown condition. If you've got some sort of polling
# class, keep a reference to it and implement a `shutdown!` that sets an
# instance variable to a state you can trigger on.
#
# Here I've (1) kept the signal handler brief and (2) catch only
# StandardError and subclasses per the ruby community's
# recommendation.
#
# (Yes, I know globals are bad, m'kay? It's an example.)
def correctish_shutdown
Signal.trap('INT') do |signo|
puts "Marking graceful shutdown on #{signo}"
$KILLED = true
end
until $KILLED do
begin
do_work
rescue => e
puts "Exception #{e.class.name} probably came from do_work."
end
end
puts 'Reached after loop, I could do proper cleanup!'
end
# This is a too-clever signal handler. Maybe it was trying to send an
# email or send a monitoring callback and something blew up or timed
# out or whatever. What happens in this case is that the signal
# handler's exception will get raised wherever your code happened to
# be running when the signal was caught. This is almost never going to
# handled right, unless you've instrumented single line of code to
# handle the exceptions your signal handler might throw.
#
# That's sarcasm. Don't do that.
#
# This is why I say keep your signal handlers short. Or you can make
# them bulletproof, but that might just mean writing "I might be
# swallowing something important code", and that's what we're trying
# to get rid of, right?
def trying_too_hard_in_handler
Signal.trap('INT') do |signo|
puts "Marking graceful shutdown on #{signo}"
raise "OH NOES I FUCKED UP"
$KILLED = true # Not reached
end
until $KILLED do
begin
do_work
rescue => e
puts "Exception #{e.class.name} came from the signal handler?!"
end
end
puts 'Never reached.'
end
#plain_jane
#sysadmin_will_hate_you
#hard_exit_fail
#hard_exit_semi_success
#correctish_shutdown
trying_too_hard_in_handler
@kylev
Copy link
Author

kylev commented Aug 11, 2015

People should also read this excellent "Graceful Shutdown" post.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment