Kaushik's Blog

Gracefully terminate a Python program with an infinite loop (and test it)

I've recently been writing long-running microservices in Python, all of which contain an infinite run loop:

def run():
  while True:
    # Do a thing
    # Do another thing
    # Do a third thing
    print("Did things")

At any time, the program could terminate midway through its operations due to an external interruption. For some applications, this may be okay. Other times, we may want to complete a loop iteration before stopping the program.

To gracefully terminate the infinite loop, we can create a flag and use Python's signal module.

# graceful_termination.py
import signal
import time

run_flag = True


def signal_handler(sig, frame):
    sig_enum = signal.Signals(sig)

    print("Program received the signal: ", sig_enum.name)
    global run_flag
    run_flag = False


signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)


def run():
    while run_flag:
        print("Press Ctrl+C")
        print("Sleeping...")
        time.sleep(0.5)
        print("Done sleeping")


if __name__ == "__main__":
    run()
    print("Exiting...")

The handler captures the SIGINT and SIGTERM interruptions1 and disables the global run_flag. If the program is in the middle of a loop iteration, all the steps are completed, then the flag is checked, the condition fails, and the program quits gracefully.

Here's what the output looks like:

$ python graceful_termination.py

Press Ctrl+C
Sleeping...
Done sleeping
Press Ctrl+C
Sleeping...
Done sleeping
Press Ctrl+C
Sleeping...
^CProgram received the signal:  SIGINT
Done sleeping
Exiting...

Neat!

Now, what if I'm stubborn about test coverage and want to test everything, including the infinitely loopy run() function?

We can do that by mocking the run_flag.

# test_graceful_termination.py
from unittest.mock import patch

from graceful_termination import run


def test_infinite_loop_run_func():

    with patch("graceful_termination.run_flag") as run_flag_mock:
        run_flag_mock.__bool__.side_effect = [True, True, False]  # Run the loop twice

        with patch("time.sleep", return_value=None) as sleep_mock:
            run()

            assert sleep_mock.call_count == 2 # Exactly 2 iterations of the loop occurred.

The side_effect specifies the value to be returned by the mock object each time it is accessed. It returns True twice, then False.

By mocking time.sleep, we can verify that the sleep function was called exactly twice.

  1. See here for an exhaustive list of signals and descriptions

#python