Starlette + asyncio.create_task() doesn't log error if task object is stored in instance variable [duplicate]

社会主义新天地 提交于 2020-03-04 18:43:23

问题


Okay, this is very weird, but here goes -

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)

Hmm, no ValueError here...


Now, remove the assignment to self.task in 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)

... And voila.


What's going on here? How does that assignment make or break the exception logging!?


回答1:


A task is a subclass of future, which means that it has the concept of a result. In case of a task, the result is the value returned by the coroutine it drives. If the coroutine raises an exception, then the exception is encapsulated in the task object. Correctly written code is expected to eventually either await the task or access its result, in order for exceptions not to pass silently.

To help debug code that forgets to access the task, task's destructor logs an error if the task raised an exception but was never awaited. This error cannot be logged before running the destructor because as long as your code holds on to the task object, it might conceivable await it at any point. The point when the destructor runs is the first instance when Python can reliably "prove" that the task is unawaited.

But this last-ditch logging is not something you should rely on, it's provided on a best-effort basis. For example, running the destructor can be postponed due to GC. I expect that assigning the task to an instance whose bound method is being driven by the task makes the task part of a reference cycle. This postpones running the destructor until a full GC, and you don't see the log.

To fix the problem, you should either catch the exceptions in your coroutine and log them yourself instead of allowing them to propagate, or actually await the task at some point in your code.




回答2:


Sorry, this was a dupe of this other question.

A drop-in replacement to asyncio.create_task() has been posted there.


Here's the output, after using the replacement create_task()

Case 1: With 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.

Case 2: Without 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


来源:https://stackoverflow.com/questions/60287285/starlette-asyncio-create-task-doesnt-log-error-if-task-object-is-stored-in

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!