Starlette + asyncio.create_task() не регистрирует ошибку, если объект задачи хранится в переменной экземпляра

Ладно, это очень странно, но вот...

import asyncio

from starlette.applications import Starlette


class MyTasks:
    def __init__(self):
        self.task = None

    async def main(self):
        self.task = asyncio.create_task(self.hello())

    async def hello(self):
        raise ValueError


async def main():
    await MyTasks().main()


app = Starlette(on_startup=[main])

$ uvicorn test:app
INFO:     Started server process [26622]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Хм, нет ValueError здесь...


Теперь удалите назначение self.task в MyTasks.main().

    async def main(self):
        asyncio.create_task(self.hello())
    ...

$ uvicorn test:app
INFO:     Started server process [29083]
INFO:     Waiting for application startup.
ERROR:    Task exception was never retrieved
future: <Task finished name='Task-3' coro=<MyTasks.hello() done, defined at ./test.py:13> exception=ValueError()>
Traceback (most recent call last):
  File "./test.py", line 14, in hello
    raise ValueError
ValueError
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

... И вуаля.


Что тут происходит? Как это назначение создает или прерывает регистрацию исключений!?


person Dev Aggarwal    schedule 18.02.2020    source источник


Ответы (2)


Задача является подклассом future, что означает что у него есть концепция результата. В случае задачи результатом является значение, возвращаемое управляемой ею сопрограммой. Если сопрограмма вызывает исключение, оно инкапсулируется в объект задачи. Ожидается, что правильно написанный код в конечном итоге либо будет ожидать выполнения задачи, либо получит доступ к ее результату, чтобы исключения не проходили молча.

Чтобы облегчить отладку кода, который забывает получить доступ к задаче, деструктор задачи записывает в журнал ошибку, если задача вызвала исключение, но никогда не ожидалась. Эта ошибка не может быть зарегистрирована до запуска деструктора, потому что, пока ваш код удерживает объект задачи, он может ожидать его в любой момент. Момент, когда запускается деструктор, является первым случаем, когда Python может надежно «доказать», что задача нежданная.

Но вы не должны полагаться на это последнее ведение журнала, оно предоставляется на основе максимальных усилий. Например, запуск деструктора может быть отложен из-за GC. Я ожидаю, что назначение задачи экземпляру, связанный метод которого управляется задачей, делает задачу частью эталонного цикла. Это откладывает запуск деструктора до полного GC, и вы не видите журнал.

Чтобы решить эту проблему, вы должны либо перехватывать исключения в своей сопрограмме и регистрировать их самостоятельно, вместо того, чтобы позволять им распространяться, либо фактически ожидать выполнения задачи в какой-то момент вашего кода.

person user4815162342    schedule 19.02.2020
comment
Ха, значит ли это, что asyncio на самом деле не поддерживает настоящие фоновые задачи из коробки? - person Dev Aggarwal; 19.02.2020
comment
Кроме того, такое поведение наблюдается только при работе внутри фреймворка. Кроме того, при использовании asyncio.run() все работает отлично! - person Dev Aggarwal; 19.02.2020
comment
@DevAggarwal Как вы пришли к выводу, что asyncio не поддерживает фоновые задачи? Все, что я написал, это то, что автоматическая регистрация неперехваченных исключений, вызванных сопрограммами задач, — это то, на что вам не следует полагаться. - person user4815162342; 19.02.2020
comment
Извините, что так прямо сформулировал, но, исходя из дротика, я ожидал, что сопрограммы будут запускаться в любое время и в любом месте, и ожидал, что они будут сообщать об исключениях независимо от того, ждал я их или нет :( - person Dev Aggarwal; 19.02.2020
comment
Кстати, есть ли какие-либо документы по этому дрянному поведению ведения журнала в документации по python или в списке рассылки? - person Dev Aggarwal; 19.02.2020
comment
Нашел - docs.python.org/3 /library/ Спасибо, что указали мне правильное направление. - person Dev Aggarwal; 19.02.2020

Извините, это был обман этого другой вопрос.

Там была опубликована замена asyncio.create_task() .


Вот результат после замены create_task()

Случай 1: с await self.task

$ uvicorn test:app
INFO:     Started server process [33213]
INFO:     Waiting for application startup.
ERROR:    Traceback (most recent call last):
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/starlette/routing.py", line 517, in lifespan
    await self.startup()
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/starlette/routing.py", line 494, in startup
    await handler()
  File "./test.py", line 35, in main
    await MyTasks().main()
  File "./test.py", line 27, in main
    print(await self.task)
  File "./test.py", line 13, in wrapper
    return await task
  File "./test.py", line 30, in hello
    raise ValueError
ValueError

ERROR:    Application startup failed. Exiting.

Случай 2: Без await self.task

$ uvicorn test:app
INFO:     Started server process [32627]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
ERROR:    Exception in callback <function create_task.<locals>.on_done at 0x10c519550>
handle: <Handle create_task.<locals>.on_done created at /Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/uvicorn/main.py:382>
source_traceback: Object created at (most recent call last):
  File "/Users/dev/.virtualenvs/server-99338def/bin/uvicorn", line 8, in <module>
    sys.exit(main())
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/uvicorn/main.py", line 331, in main
    run(**kwargs)
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/uvicorn/main.py", line 354, in run
    server.run()
  File "/Users/dev/.virtualenvs/server-99338def/lib/python3.8/site-packages/uvicorn/main.py", line 382, in run
    loop.run_until_complete(self.serve(sockets=sockets))
Traceback (most recent call last):
  File "uvloop/cbhandles.pyx", line 70, in uvloop.loop.Handle._run
  File "./test.py", line 9, in on_done
    fut.result()
  File "./test.py", line 30, in hello
    raise ValueError
ValueError
person Dev Aggarwal    schedule 19.02.2020
comment
await и asyncio.create_task не совпадают. create_task позволяет выполнять задачи параллельно await нет. - person Att Righ; 04.11.2020