MrJ
MrJ

Reputation: 1514

Python asyncio program won't exit

I have an asyncio/Python program with two asyncio tasks:

I want my entire program to exit after the first crash. I cannot get it to happen.

import asyncio
import time

def infinite_while():
    while True:
        time.sleep(1)


async def task_1():
    await asyncio.sleep(1)
    assert False


async def task_2():
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, lambda: infinite_while())


loop = asyncio.get_event_loop()
asyncio.set_event_loop(loop)

tasks = asyncio.gather(task_2(), task_1())
try:
    loop.run_until_complete(tasks)
except (Exception, KeyboardInterrupt) as e:
    print('ERROR', str(e))
    exit()

It prints ERROR but does not exit. When manually closed, the program prints the following stack trace:

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/usr/lib/python3.5/concurrent/futures/thread.py", line 39, in _python_exit
    t.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

Upvotes: 15

Views: 13934

Answers (1)

bagrat
bagrat

Reputation: 7468

When an exception is risen in a task, it never propagates to the scope where the task was launched via eventloop, i.e. the loop.run_until_complete(tasks) call. Think of it, as if the exception is thrown only in the context of your task, and that is the top level scope where you have the chance to handle it, otherwise it will be risen in the "background".

This said, you will never catch an Exception from the task with this:

try:
    loop.run_until_complete(tasks)
except (Exception, KeyboardInterrupt) as e:
    print('ERROR', str(e))
    exit()

...and that is just how the event loop works. Imagine if you would have a service with several tasks, and one of them would fail, that would stop the whole service.

What you could do is stop the eventloop manually when you catch an exception in task1, e.g.

async def task_1():
    await asyncio.sleep(1)
    try:
        assert False
    except Exception:
        # get the eventloop reference somehow
        eventloop.stop()

However, this is very dirty and kind of hacky, so I would rather suggest to go with the solution that @D-Von suggested, which is much cleaner and safer.

Upvotes: 7

Related Questions