6 minute read

This post explains how to support graceful shutdowns in your python application by responding to OS signals via the kill command.

Kill Command

Unix supports inter-process communication through signals. That is, one process can send a signal to another process. Sending a signal can be initiated by using the unix kill command1.

The syntax of kill is: kill -signal_number PID. For example, to send the SIGKILL signal to the process with PID 123, issue the following:

kill -9 123

A list of signals and how to discover a process ID (PID) can be found below.

Abbreviated List of Supported Signals

Signal Value Intended Role
SIGINT 2 Interrupt from keyboard (Ctrl-C). Request application stop.
SIGQUIT 3 Terminal quit signal. Request application stop w/core dump.
SIGABRT 6 Abort. Request application stop w/core dump.
SIGKILL 9 Kill. This cannot be caught by application.
SIGTERM 15 Termination signal. Request application stop.

A more comprehensive list can be found: Signal on Wikipedia

How to discover PID

The ps command can be used to discover process ID.

% ps -ef 
  501 19894 19893   0  6:51PM ttys001    0:00.07 -zsh
  501 20213 49025   0  6:57PM ttys008    0:00.07 /bin/zsh -il
  501 20244 49025   0  6:57PM ttys012    0:00.01 /bin/zsh
    0 24816  3822   0  8:02PM ttys014    0:00.02 login -pfl jasonray /bin/bash -c exec -la zsh /bin/zsh
  501 24817 24816   0  8:02PM ttys014    0:00.07 -zsh
  501 25719 24817   0  8:49PM ttys014    0:00.00 grep zsh

Suppose that you are attempting to find the process ID for VS Code:

% ps -ef | grep Code.ap
  501 20401 20211   0  6:57PM ??         2:00.56 /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Code Helper (Plugin) --ms-enable-electron-run-as-node /Users/jasonray/.vscode/extensions/ms-python.vscode-pylance-2023.11.10/dist/server.bundle.js --cancellationReceive=file:81c4264013be4acee258d4cb2932b5f1ccb32e92ce --node-ipc --clientProcessId=20211

The second column (20401) is the PID.

How to send signal

With the PID, run the following command to kill the process:

kill -9 20401

How to support a kill command in your application

Suppose that you have a long running application. Perhaps it is monitoring a queue: read from queue, process message, repeat. This could run for minutes, hours, forever. If you wanted to terminate this process, you could:

  • Send kill -9 to the process. This will immediately kill the process.

In some scenarios, this works fine. However, there is a drawback - there is no opportunity for the application to gracefully shutdown, ensuring consistency / releasing resources.

Instead, we can modify the application to react to a kill command, gracefully shutdown, and then allow the process to cleanly terminate.

Which signals to monitor

SIGKILL will not be received by the application - the OS kills the process without opportunity for the application to react. This can be helpful if the application is hung.

I generally listen for: SIGINT, SIGTERM

General approach

  1. On application start, register listener for relevant signals.
  2. In the listener, when a signal is received, set a application-wide variable to indicate “shutdown requested”.
  3. Within main processing loop, check for “shutdown requested” variable, and if so abort loop.

How to handle shutdown signal in Python

  1. Create a variable to represent “shutdown requested”. This could be a global or a class level variable.
    shutdown_requested = False
    
  2. Create a handle method which accepts two params: signum and frame. Upon invocation, set the “shutdown requested” variable.
    def handler(signum, frame):
     global shutdown_requested
     print('Signal handler called with signal, requesting shutdown', signum)
     shutdown_requested=True
    
  3. Register the handler for each of the relevant signals2.
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGTERM, handler)
    
  4. Modify main loop to check for the “shutdown requested” variable. This check can be used to finish up work in progress, exit the main loop, and then release resources.
    while not shutdown_requested:
     print('doing work, processing stuff, being useful')
    

Final Python Code to handle shutdown

How to handle shutdown signal in Java

A static volatile boolean variable shutdownRequested is used to track whether a shutdown has been requested. The volatile keyword ensures that changes to this variable are immediately visible to all threads.

A shutdown hook is registered using Runtime.getRuntime().addShutdownHook(…). This hook sets shutdownRequested to true. Shutdown hooks are called in response to a JVM shutdown, which can be triggered by SIGTERM or a normal program termination.

The main loop of the program checks shutdownRequested to decide whether to continue processing.

When shutdownRequested becomes true (either due to a normal shutdown or a termination signal like SIGTERM), the program exits the loop and can perform any necessary cleanup.

Remember that this code won’t respond to SIGKILL or SIGSTOP, as Java doesn’t provide a way to handle these signals.

Final Java Code to handle shutdown

How to handle shutdown in node.js

In Node.js, you can handle graceful shutdowns similarly to how it’s done in Python, by listening for specific signals and then performing cleanup actions before exiting the program. Node.js provides the process object which emits events corresponding to various signals. You can set up listeners for these events to implement graceful shutdown.

Here’s an equivalent Node.js code snippet that demonstrates handling graceful shutdown:

In this code:

We use process.on(‘SIGINT’, handleSignal) and process.on(‘SIGTERM’, handleSignal) to listen for SIGINT and SIGTERM signals. SIGINT is typically sent when the user presses Ctrl+C, and SIGTERM is a termination signal commonly sent from system tools like kill.

The handleSignal function is a signal handler that sets the shutdownRequested flag to true upon receiving a signal.

The mainLoop function represents the main work loop of the application. If shutdownRequested is false, it continues processing and schedules the next loop iteration with setTimeout. If shutdownRequested is true, it performs necessary cleanup and exits the process.

The loop is initiated by calling mainLoop().

This Node.js script will react to SIGINT and SIGTERM by setting the shutdownRequested flag and will perform a graceful shutdown when the flag is set. It’s a common pattern for handling graceful shutdowns in Node.js applications.

Final Node.js Code to handle shutdown

How to handle shutdown in golang

In Go (Golang), you can handle graceful shutdowns by listening for OS signals using the os/signal package. This allows your Go application to intercept signals like SIGINT and SIGTERM for a graceful shutdown, where you can clean up resources before exiting.

Here’s an equivalent Go program that demonstrates handling graceful shutdowns:

In this Go code:

We create a channel sigChan to receive OS signals.

We use signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) to relay SIGINT and SIGTERM signals to sigChan. SIGINT is generally sent when the user presses Ctrl+C, and SIGTERM is sent by system commands like kill to request termination.

We start a goroutine that waits for a signal on sigChan. When a signal is received, it prints a message and sends a value to the done channel.

The main loop of the program uses a select statement to either continue doing work (in the default case) or to perform a graceful shutdown if the done channel receives a value.

When the done channel receives a value, the program prints a shutdown message and exits the loop to allow the program to terminate.

This pattern is a common and effective way to handle graceful shutdowns in Go applications.

Final Golang Code to handle shutdown

  1. The term kill seems to be a misnomer. Although it is likely that the first use of signals was to terminate (kill) a process, it seems to have grown to other inter-process commands beyond kill. It is possible to use signals for many other purposes, and to use the kill command as a means to send those signals. 

  2. Further reading on Python handling of signals: https://docs.python.org/3/library/signal.html