The problem (I think)
The contextlib.asynccontextmanager
documentation gives this example:
@asynccontextmanager
async def get_connection():
conn = await acquire_db_connection()
try:
yield conn
finally:
await release_db_connection(conn)
It looks to me like this can leak resources. If this code's task is cancelled while this code is on its await release_db_connection(conn)
line, the release could be interrupted. The asyncio.CancelledError
will propagate up from somewhere within the finally
block, preventing subsequent cleanup code from running.
So, in practical terms, if you're implementing a web server that handles requests with a timeout, a timeout firing at the exact wrong time could cause a database connection to leak.
Full runnable example
import asyncio
from contextlib import asynccontextmanager
async def acquire_db_connection():
await asyncio.sleep(1)
print("Acquired database connection.")
return "<fake connection object>"
async def release_db_connection(conn):
await asyncio.sleep(1)
print("Released database connection.")
@asynccontextmanager
async def get_connection():
conn = await acquire_db_connection()
try:
yield conn
finally:
await release_db_connection(conn)
async def do_stuff_with_connection():
async with get_connection() as conn:
await asyncio.sleep(1)
print("Did stuff with connection.")
async def main():
task = asyncio.create_task(do_stuff_with_connection())
# Cancel the task just as the context manager running
# inside of it is executing its cleanup code.
await asyncio.sleep(2.5)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
print("Done.")
asyncio.run(main())
Output on Python 3.7.9:
Acquired database connection.
Did stuff with connection.
Done.
Note that Released database connection
is never printed.
My questions
- This is a problem, right? Intuitively to me, I expect
.cancel()
to mean "cancel gracefully, cleaning up any resources used along the way." (Otherwise, why would they have implemented cancellation as exception propagation?) But I could be wrong. Maybe, for example,.cancel()
is meant to be fast instead of graceful. Is there an authoritative source that clarifies what.cancel()
is supposed to do here? - If this is indeed a problem, how do I fix it?
CancelledError
implies that you can still do the cleanup yourself -- but you'd have to cache the equivalent of theconn
state somewhere accessible to other parts of the code. – Synsepalous