web-dev-qa-db-ja.com

Pythonでの非同期例外処理

asyncioおよびaiohttpを使用して非同期HTTP要求を作成する次のコードを作成しました。

import sys
import asyncio
import aiohttp

@asyncio.coroutine
def get(url):
    try:
        print('GET %s' % url)
        resp = yield from aiohttp.request('GET', url)
    except Exception as e:
        raise Exception("%s has error '%s'" % (url, e))
    else:
        if resp.status >= 400:
            raise Exception("%s has error '%s: %s'" % (url, resp.status, resp.reason))

    return (yield from resp.text())

@asyncio.coroutine
def fill_data(run):
    url = 'http://www.google.com/%s' % run['name']
    run['data'] = yield from get(url)

def get_runs():
    runs = [ {'name': 'one'}, {'name': 'two'} ]
    loop = asyncio.get_event_loop()
    task = asyncio.wait([fill_data(r) for r in runs])
    loop.run_until_complete(task)   
    return runs

try:
    get_runs()
except Exception as e:
    print(repr(e))
    sys.exit(1)

何らかの理由で、get関数内で発生した例外はキャッチされません。

Future/Task exception was never retrieved
Traceback (most recent call last):
  File "site-packages/asyncio/tasks.py", line 236, in _step
    result = coro.send(value)
  File "mwe.py", line 25, in fill_data
    run['data'] = yield from get(url)
  File "mwe.py", line 17, in get
    raise Exception("%s has error '%s: %s'" % (url, resp.status, resp.reason))
Exception: http://www.google.com/two has error '404: Not Found'

それでは、ルーチンによって発生した例外を処理する正しい方法は何ですか?

37
Yury Bayda

_asyncio.wait_は実際に渡されたFuturesを消費せず、それらが完了するのを待ってからFutureオブジェクトを返します。

コルーチンasyncio.wait(futures, *, loop=None, timeout=None, return_when=ALL_COMPLETED)

シーケンスfuturesによって指定されたFuturesおよびコルーチンオブジェクトが完了するまで待ちます。コルーチンはタスクにラップされます。 Futureの2つのセットを返します:(完了、保留中)。

doneリストの項目を実際に_yield from_するまで、それらは消費されないままです。プログラムは先物を消費せずに終了するため、「例外は取得されませんでした」というメッセージが表示されます。

ユースケースでは、おそらく _asyncio.gather_ を使用する方が理にかなっています。これは、各Futureを実際に消費し、すべての結果を集約する(または最初のFutureを上げる単一のExceptionを返します)入力リストの未来によってスローされます)。

_def get_runs():
    runs = [ {'name': 'one'}, {'name': 'two'} ]
    loop = asyncio.get_event_loop()
    tasks = asyncio.gather(*[fill_data(r) for r in runs])
    loop.run_until_complete(tasks)
    return runs
_

出力:

_GET http://www.google.com/two
GET http://www.google.com/one
Exception("http://www.google.com/one has error '404: Not Found'",)
_

_asyncio.gather_を使用すると、futureの1つが例外を発生させたときの動作を実際にカスタマイズできることに注意してください。デフォルトの動作では、最初にヒットした例外が発生しますが、出力リストで各例外オブジェクトを返すこともできます。

asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False)

指定されたコルーチンオブジェクトまたはフューチャからフューチャ集計結果を返します。

すべての先物は同じイベントループを共有する必要があります。すべてのタスクが正常に完了した場合、返されるフューチャーの結果は結果のリストです(元のシーケンスの順序で、必ずしも結果の到着順ではありません)。 _return_exceptions_がTrueの場合、タスクの例外は正常な結果と同様に処理され、結果リストに収集されます。そうでない場合、最初に発生した例外は、返されたfutureにすぐに伝播されます。

44
dano

callback で例外をデバッグまたは「処理」するには:

結果を返すか、例外を発生させるコルーチン:

@asyncio.coroutine
def async_something_entry_point(self):
    try:
        return self.real_stuff_which_throw_exceptions()
    except:
        raise Exception(some_identifier_here + ' ' + traceback.format_exc())

そしてコールバック:

def callback(self, future: asyncio.Future):
    exc = future.exception()
    if exc:
        # Handle wonderful empty TimeoutError exception
        if type(exc) == TimeoutError:
            self.logger('<Some id here> callback exception TimeoutError')
        else:
            self.logger("<Some id here> callback exception " + str(exc))

    # store your result where you want
    self.result.append(
        future.result()
    )
2
Oleg Neumyvakin