» Handle with care!
SignalException
is not your everyday exception. It has a specific purpose and is raised when the current application's process receives a signal from the OS (in some rare cases, the current application can also raise this exception).
Every SignalException
contains a signo
(an integer signal number), and each signal number has a specific meaning and a string identifier associated with it. For instance, the SIGTERM
string identifier has an associated integer value of 15
, and is often used to notify an application to clean up and stop.
One example for this is: When a computer is shutting down, the OS initially sends out a SIGTERM(15)
signal to all the running applications, and well behaving applications respond to this by closing their resources, persisting unsaved data and shutting down in a clean fashion. If the applications don't stop in time, the operating system will forcibly shut them down using the SIGKILL(9)
signal. You may have used a kill -9
many times without knowing that it actually sends a SIGKILL
signal to the running application which in turn forcibly shuts the application down, without giving it time to clean up any resources. SIGKILL
is a SignalException
which cannot be caught and handled by your application.
Let's look at a quick example:
# signal.rb
begin
puts "Started process: #{Process.pid}"
sleep # wait for the interrupt from outside
rescue SignalException => e
puts "received Exception #{e}"
end
ruby signal.rb
# => Started process: 23498
kill -s TERM 23498
# => received Exception SIGTERM
» How to trap it
You can trap a signal exception in the following ways:
» Using Signal.trap
The Signal.trap
method allows you to define the signal that you want to trap and run a block when such a signal is received:
# trap_signal.rb
def run
puts 'Running my app...'
sleep # sleep indefinitely to simulate work
end
def cleanup_and_exit
puts 'Oops! Need to shut things down...'
puts 'Closing up database connections...'
puts 'Persisting unsaved data...'
exit
end
Signal.trap('TERM') { cleanup_and_exit }
Signal.trap('INT') { cleanup_and_exit }
run # trigger our run function
ruby trap_signal.rb
# => Running my app...
# => Hit Ctrl+C on the keyboard
# => Oops! Need to shut things down...
# => Closing up database connections...
# => Persisting unsaved data...
In the above example when you run the app and hit Ctrl+C
on your keyboard, it will be caught by the Signal.trap('INT')
line and execute the cleanup_and_exit
function which will then clean up all the resources and exit (in this case it doesn't really do anything; your real app would have code which closes logs or database connections, etc.).
» Rescuing SignalException
# rescue_signal.rb
def cleanup_and_exit(exception)
puts "Received a #{exception}"
puts 'Oops! need to shut things down...'
puts 'Closing up database connections...'
puts 'Persisting unsaved data...'
exit
end
def run
puts "Running my app. PID: #{Process.pid}"
sleep # sleep indefinitely to simulate work
rescue SignalException => ex
cleanup_and_exit(ex)
end
run # trigger our run function
# terminal-1
$ ruby ~/s/rescue_signal.rb
Running my app. PID: 2552
# from a different terminal: terminal-2
$ kill -s TERM 2552
# terminal-1
Received a SIGTERM
Oops! need to shut things down...
Closing up database connections...
Persisting unsaved data...
This script behaves in the same way as the trap_signal.rb
script.
So, If you are building an app which needs to tidy up things before shutting down, SignalException
s are a great way to handle that.
» Signal handling guidelines
- Signal Handling should be done thoughtfully; some poorly designed apps ignore signals like
SIGINT
, which leads to frustrated users who unsuccessfully try to close the app usingCtrl+C
. - Some signals like the
SIGKILL(9)
cannot be trapped as they are meant to forcibly shut down the application when all else fails. - The signal handler should be fast. The OS gives most applications just a small amount of time before it forcibly stops them. So, be mindful of this fact and do the bare minimum that is necessary. For instance, writing to a log or finishing your database transactions is good. However, doing time consuming computations or uploading large chunks of data to remote servers is not advisable.
- Use the right signal for the job: each signal has semantics attached to it, use them for the right job. A good example is Puma, the ruby web server which uses
SIGTERM
to shut down workers. Signal.trap
clobbers previous signal handlers. So, use it with care. If you are writing a library, make sure you have a strong reason to use it.
» Some good examples
Look at the way well-written apps like puma, unicorn and nginx handle signals.
Some good use cases from the above examples are:
SIGQUIT
for graceful shutdown (nginx).SIGTERM
for fast shutdown (nginx).SIGHUP
for reloading configuration (nginx).SIGUSR1
for re-opening log files (nginx).SIGTTIN
increment the number of worker processes by one (nginx, unicorn, puma).SIGTTOU
decrement the number of worker processes by one (nginx, unicorn, puma).
» Full list of signals
Signal.list
will list the string identifiers and the integer values of all the signals supported by your operating system.
# Signals on a Linux computer
EXIT , 0
HUP , 1
INT , 2
QUIT , 3
ILL , 4
TRAP , 5
ABRT , 6
IOT , 6
BUS , 7
FPE , 8
KILL , 9
USR1 , 10
SEGV , 11
USR2 , 12
PIPE , 13
ALRM , 14
TERM , 15
CHLD , 17
CLD , 17
CONT , 18
STOP , 19
TSTP , 20
TTIN , 21
TTOU , 22
URG , 23
XCPU , 24
XFSZ , 25
VTALRM , 26
PROF , 27
WINCH , 28
IO , 29
POLL , 29
PWR , 30
SYS , 31