Skip to content

Instantly share code, notes, and snippets.

@nvgoldin
Created July 27, 2016 13:34
Show Gist options
  • Save nvgoldin/30cea3c04ee0796ebd0489aa62bcf00a to your computer and use it in GitHub Desktop.
Save nvgoldin/30cea3c04ee0796ebd0489aa62bcf00a to your computer and use it in GitHub Desktop.
Python 3.5 asyncio - shutdown all tasks safely using signal handler
import signal
import functools
async def looping_task(loop, task_num):
try:
while True:
print('{0}:in looping_task'.format(task_num))
await asyncio.sleep(5.0, loop=loop)
except asyncio.CancelledError:
return "{0}: I was cancelled!".format(task_num)
async def shutdown(sig, loop):
print('caught {0}'.format(sig.name))
tasks = [task for task in asyncio.Task.all_tasks() if task is not
asyncio.tasks.Task.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()
loop = asyncio.get_event_loop()
for i in range(5):
asyncio.ensure_future(looping_task(loop, i), loop=loop)
loop.add_signal_handler(signal.SIGTERM,
functools.partial(asyncio.ensure_future,
shutdown(signal.SIGTERM, loop)))
try:
loop.run_forever()
finally:
loop.close()
@stanislavhordiyenko
Copy link

Thank you, works like a charm.

@CarstenMaul
Copy link

For catching CTRL+C catch signal.SIGINT

@rjjanuary
Copy link

Is there a similar method we might be able to utilize in further python versions? eg: >= 3.8?
This code now raises the following error:

/usr/local/Cellar/[email protected]/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/unix_events.py:140: RuntimeWarning: coroutine 'shutdown' was never awaited
del self._signal_handlers[sig]
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Traceback (most recent call last):
File "/Users/rjanuary/git/stc-ds-sfbridge/at.py", line 31, in
loop.run_forever()
File "/usr/local/Cellar/[email protected]/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 570, in run_forever
self._run_once()
File "/usr/local/Cellar/[email protected]/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/base_events.py", line 1823, in _run_once
event_list = self._selector.select(timeout)
File "/usr/local/Cellar/[email protected]/3.8.6/Frameworks/Python.framework/Versions/3.8/lib/python3.8/selectors.py", line 558, in select
kev_list = self._selector.control(None, max_ev, timeout)
KeyboardInterrupt

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

@ShaneEverittM
Copy link

@rjjanuary Ever figure it out? Exact same thing for me.

@vsinha
Copy link

vsinha commented Nov 17, 2022

Something like this works for me in 3.10. I like nvgoldin's functional style but i find it clunky to read in python and with python-black it's one less line to just do it the imperative way

def add_signal_handlers():
    loop = asyncio.get_event_loop()

    async def shutdown(sig: signal.Signals) -> None:
        """
        Cancel all running async tasks (other than this one) when called.
        By catching asyncio.CancelledError, any running task can perform
        any necessary cleanup when it's cancelled.
        """
        tasks = []
        for task in asyncio.all_tasks(loop):
            if task is not asyncio.current_task(loop):
                task.cancel()
                tasks.append(task)
        results = await asyncio.gather(*tasks, return_exceptions=True)
        print("Finished awaiting cancelled tasks, results: {0}".format(results))
        loop.stop()

    for sig in [signal.SIGINT, signal.SIGTERM]:
        loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown(sig)))

@auspex
Copy link

auspex commented May 5, 2025

I got this working for SIGINT, but I had that working with much simpler code that just did:

def signal_handler(sig, frame):
    global group
    group.cancel()

async def main():
    global group

    group = asyncio.gather(
            task1(),
            task2(),
    )
    await group

if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    try:
        asyncio.run(main())
    except asyncio.exceptions.CancelledError:
        pass

SIGTERM never worked for that, but it isn't working with this method either! SIGTERM never enters my signal handler.

My code is a bit of a hybrid of the two examples above, as much of the original doesn't work under python 3.12:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment