web-dev-qa-db-fra.com

Gestion des exceptions asynchrones dans Python

J'ai le code suivant en utilisant asyncio et aiohttp pour effectuer des requêtes HTTP asynchrones.

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)

Pour une raison quelconque, les exceptions déclenchées à l'intérieur de la fonction get ne sont pas interceptées:

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'

Alors, quelle est la bonne façon de gérer les exceptions soulevées par les couroutines?

37
Yury Bayda

asyncio.wait Ne consomme pas réellement le Futures qui lui est passé, il attend juste qu'ils se terminent, puis retourne les objets Future:

coroutineasyncio.wait(futures, *, loop=None, timeout=None, return_when=ALL_COMPLETED)

Attendez que les objets Futures et coroutine donnés par les futurs séquences se terminent. Les coroutines seront enveloppées dans des tâches. Renvoie deux ensembles de Future: (terminé, en attente).

Jusqu'à ce que vous ayez réellement yield from Les éléments de la liste done, ils ne seront pas consommés. Étant donné que votre programme se ferme sans consommer les futurs, vous voyez les messages "exception n'a jamais été récupérée".

Pour votre cas d'utilisation, il est probablement plus logique d'utiliser asyncio.gather , qui consommera réellement chaque Future, puis renverra un seul Future qui agrège tous leurs résultats (ou lève le premier Exception lancé par un futur dans la liste d'entrée).

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

Sortie:

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

Notez que asyncio.gather Vous permet en fait de personnaliser son comportement lorsque l'un des futurs lève une exception; le comportement par défaut consiste à déclencher la première exception qu'il frappe, mais il peut également simplement renvoyer chaque objet d'exception dans la liste de sortie:

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

Retourne un futur regroupant les résultats des objets ou futurs coroutine donnés.

Tous les futures doivent partager la même boucle d'événements. Si toutes les tâches sont effectuées avec succès, le résultat futur renvoyé est la liste des résultats (dans l’ordre de la séquence d’origine, pas nécessairement dans l’ordre d’arrivée des résultats). Si return_exceptions Est True, les exceptions dans les tâches sont traitées de la même manière que les résultats réussis et rassemblées dans la liste des résultats; sinon, la première exception levée sera immédiatement propagée dans le futur retourné.

44
dano

Pour déboguer ou "gérer" les exceptions dans rappel :

Coroutine qui retourne un résultat ou déclenche des exceptions:

@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())

Et rappel:

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