web-dev-qa-db-ja.com

コルーチンがasyncioで例外を発生させた場合、ループをシャットダウンしてエラーを出力するにはどうすればよいですか?

いくつかのコルーチンがループで実行されているとします。それらのいくつかが例外で失敗した場合、プログラム全体がこの例外で失敗するようにするにはどうすればよいですか?現在、ログレベル「DEBUG」を使用しない限り、asyncioはコルーチンからのエラーメッセージを出力しません。

from asyncio import get_event_loop, sleep


async def c(sleep_time=2, fail=False):
    print('c', sleep_time, fail)
    if fail:
        raise Exception('fail')
    while True:
        print('doing stuff')
        await sleep(sleep_time)



loop = get_event_loop()
loop.create_task(c(sleep_time=10, fail=False))
loop.create_task(c(fail=True))
loop.run_forever()
14
user1685095

優雅な方法は、エラー処理APIを使用することです。

https://docs.python.org/3/library/asyncio-eventloop.html#error-handling-api

例:

import asyncio


async def run_division(a, b):
    await asyncio.sleep(2)
    return a / b


def custom_exception_handler(loop, context):
    # first, handle with default handler
    loop.default_exception_handler(context)

    exception = context.get('exception')
    if isinstance(exception, ZeroDivisionError):
        print(context)
        loop.stop()

loop = asyncio.get_event_loop()

# Set custom handler
loop.set_exception_handler(custom_exception_handler)
loop.create_task(run_division(1, 0))
loop.run_forever()
8
Char

ソリューションを作成するために使用する可能性のあるメモを次に示します。

コルーチンの例外(または結果!)を取得する最も簡単な方法は、それをawaitすることです。 asyncio.gather()は、コルーチンからタスクを作成し、サブタスクの1つが失敗した場合に失敗する1つの包括的なタスクにそれらすべてをラップします。

_import asyncio

import random


async def coro(n):
    print("Start", n)
    await asyncio.sleep(random.uniform(0.2, 0.5))
    if n % 4 == 0:
        raise Exception('fail ({})'.format(n))
    return "OK: {}".format(n)


async def main():
    tasks = [coro(i) for i in range(10)]
    await asyncio.gather(*tasks)
    print("done")

loop = asyncio.get_event_loop()
try:
    asyncio.ensure_future(main())
    loop.run_forever()
finally:
    loop.close()
_

ただし、これによってループがシャットダウンされることはありません。実行中のループを停止するには、loop.stop()を使用します。代わりにこれを使用してください:

_async def main():
    tasks = [coro(i) for i in range(10)]
    try:
        await asyncio.gather(*tasks)
    except Exception as e:
        loop.stop()
        raise
    print("done")
_

実行時間の長いコルーチンの実行中にループを停止することは、おそらくあなたが望むことではありません。最初に、イベントを使用してシャットダウンするようにコルーチンに信号を送ることをお勧めします。

_import asyncio

import random


async def repeat(n):
    print("start", n)
    while not shutting_down.is_set():
        print("repeat", n)
        await asyncio.sleep(random.uniform(1, 3))
    print("done", n)


async def main():
    print("waiting 6 seconds..")
    await asyncio.sleep(6)
    print("shutting down")
    shutting_down.set()  # not a coroutine!
    print("waiting")
    await asyncio.wait(long_running)
    print("done")
    loop.stop()

loop = asyncio.get_event_loop()
shutting_down = asyncio.Event(loop=loop)
long_running = [loop.create_task(repeat(i + 1))  for i in range(5)]
try:
    asyncio.ensure_future(main())
    loop.run_forever()
finally:
    loop.close()
_

タスクにawaitを使用したくない場合は、_asyncio.Event_(または_asyncio.Queue_)を使用して、グローバルエラーハンドラーに信号を送り、ループを停止することをお勧めします。

_import asyncio


async def fail():
    try:
        print("doing stuff...")
        await asyncio.sleep(0.2)
        print("doing stuff...")
        await asyncio.sleep(0.2)
        print("doing stuff...")
        raise Exception('fail')
    except Exception as e:
        error_event.payload = e
        error_event.set()
        raise  # optional


async def error_handler():
    await error_event.wait()
    e = error_event.payload
    print("Got:", e)
    raise e


loop = asyncio.get_event_loop()
error_event = asyncio.Event()
try:
    loop.create_task(fail())
    loop.run_until_complete(error_handler())
finally:
    loop.close()
_

(ここでは簡単にするためにrun_until_complete()と一緒に使用しますが、loop.stop()と一緒に使用することもできます)

3
Udi

さて、既存のコードを書き直す必要のないソリューションを見つけました。ハッキーに見えるかもしれませんが、私はそれが好きだと思います。

私はすでにKeyboardInterruptをそのように捕まえているので。

def main(task=None):
    task = task or start()
    loop = get_event_loop()
    try:
        task = loop.create_task(task)
        future = ensure_future(task)
        loop.run_until_complete(task)
    except KeyboardInterrupt:
        print('\nperforming cleanup...')
        task.cancel()
        loop.run_until_complete(future)
        loop.close()
        sys.exit()

コルーチンからKeyboardInterruptを自分自身に送信するのはどうですか? os.killはアプリケーションが閉じるのを待ち、待つアプリケーションは同じアプリケーションであるため、アプリケーションがハングするだろうと思いましたが、ありがたいことに私は間違っていました。そして、このコードは実際に機能し、終了する前にclean upを出力します。

async def c(sleep_time=2, fail=False):
    print('c', sleep_time, fail)
    if fail:
        await sleep(sleep_time)
        os.kill(os.getpid(), signal.SIGINT)
    while True:
        print('doing stuff')
        await sleep(sleep_time)



loop = get_event_loop()
loop.create_task(c(sleep_time=10, fail=False))
loop.create_task(c(fail=True))
try:
    loop.run_forever()
except KeyboardInterrupt:
    print('clean up')
    loop.close()
    sys.exit()
1
user1685095