Based on this code, I'm trying to catch SIGINT and SIGTERM. It works perfectly for SIGINT: I see it enter the signal handler, then my tasks do their cleanup before the whole program exits. On SIGTERM, though, the program simply exits immediately.
My code is a bit of a hybrid of the two examples from the link above, as much of the original doesn't work under python 3.12:
import asyncio
import functools
import signal
async def signal_handler(sig, loop):
"""
Exit cleanly on SIGTERM ("docker stop"), SIGINT (^C when interactive)
"""
print('caught {0}'.format(sig.name))
tasks = [task for task in asyncio.all_tasks() if task is not
asyncio.current_task()]
list(map(lambda task: task.cancel(), tasks))
results = await asyncio.gather(*tasks, return_exceptions=True)
print('finished awaiting cancelled tasks, results: {0}'.format(results))
loop.stop()
if __name__ == "__main__":
loop = asyncio.new_event_loop()
asyncio.ensure_future(task1(), loop=loop)
asyncio.ensure_future(task2(), loop=loop)
loop.add_signal_handler(signal.SIGTERM,
functools.partial(asyncio.ensure_future,
signal_handler(signal.SIGTERM, loop)))
loop.add_signal_handler(signal.SIGINT,
functools.partial(asyncio.ensure_future,
signal_handler(signal.SIGINT, loop)))
try:
loop.run_forever()
finally:
loop.close()
task1
can terminate immediately, but task2
has cleanup code that is clearly being executed after SIGINT, but not after SIGTERM
That gist is very old, and asyncio
/python has evolved since.
Your code sort of works, but the way it's designed, the signal handling will create two coroutines, one of which will not be awaited when the other signal is received. This is because the couroutines are eagerly created, but they're only launched (ensure_future
) when the corresponding signal is received.
Thus, SIGTERM
will be properly handled, but python will complain with RuntimeWarning: corouting 'signal_handler' was never awaited
.
A more modern take on your version might look something like:
import asyncio
import signal
async def task1():
try:
while True:
print("Task 1 running...")
await asyncio.sleep(1)
except asyncio.CancelledError:
print("Task 1 cancelled")
# Task naturally stops once it raises CancelledError.
async def task2():
try:
while True:
print("Task 2 running...")
await asyncio.sleep(1)
except asyncio.CancelledError:
print("Task 2 cancelled")
# Need to pass the list of tasks to cancel so that we don't kill the main task.
# Alternatively, one could pass in the main task to explicitly exclude it.
async def shutdown(sig, tasks):
print(f"Caught signal: {sig.name}")
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
print("Shutdown complete.")
async def main():
tasks = [asyncio.create_task(task1()), asyncio.create_task(task2())]
loop = asyncio.get_running_loop()
for s in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(s, lambda s=s: asyncio.create_task(shutdown(s, tasks)))
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
$ timeout 2s python3 sigterm.py
Task 1 running...
Task 2 running...
Task 1 running...
Task 2 running...
Caught signal: SIGTERM
Task 1 cancelled
Task 2 cancelled
Shutdown complete.
In this particular case, though, I'd probably use a stop event or similar to signal the tasks to exit:
import signal
import asyncio
stop_event = asyncio.Event()
def signal_handler():
print("SIGTERM received! Exiting...")
stop_event.set()
async def looping_task(task_num):
while not stop_event.is_set():
print(f"Task {task_num} is running...")
await asyncio.sleep((task_num + 1) / 3)
async def main():
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGTERM, signal_handler)
await asyncio.gather(*(looping_task(i) for i in range(5)))
if __name__ == "__main__":
asyncio.run(main())