Last active
August 29, 2015 14:26
-
-
Save kylev/68f69ecfed9f48fc055b to your computer and use it in GitHub Desktop.
An exploration of common pitfalls in Ruby Exception/Signal handling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
People should also read this excellent "Graceful Shutdown" post.