» 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, SignalExceptions 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
SIGTERMto shut down workers. Signal.trapclobbers 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:
SIGQUITfor graceful shutdown (nginx).SIGTERMfor fast shutdown (nginx).SIGHUPfor reloading configuration (nginx).SIGUSR1for re-opening log files (nginx).SIGTTINincrement the number of worker processes by one (nginx, unicorn, puma).SIGTTOUdecrement 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