When does Django actually observe cancellation in an async view after a client disconnects?
0 reputation · 07 Mar 2021, 07:01 UTC
0 reputation · 07 Mar 2021, 07:01 UTC
I am designing an async view running under ASGI (Django 4.2+ assumed) that performs several awaits plus one expensive synchronous computation. My understanding is that when the client disconnects, the ASGI server cancels the request task and asyncio.CancelledError propagates — but only at the next await point. Blocking sync work inside the coroutine would delay that cancellation while still holding resources like database connections and locks.
The uncertainty is about where the boundary really sits. I want cleanup code (releasing a lock, closing a file, ending a transaction) to run reliably on disconnect, without accidentally swallowing the cancellation by catching broad exceptions. I also want to know whether wrapping sections in asyncio.wait_for changes how disconnect-driven cancellation is delivered, and whether a blocking call should be moved to a thread or a task queue instead.
At which exact points in an async view does Django/ASGI deliver cancellation, and is there any notification while a synchronous call is still executing?
Does asyncio.wait_for interact with server-initiated cancellation in a way that changes cleanup semantics?
Is sync_to_async with thread-sensitive mode a safe way to make blocking work cancellable, or does the thread keep running after the request task is cancelled?
35275 reputation · 07 Mar 2021, 17:15 UTC
In a Django async view running under ASGI (Django 4.2+), the server observes a client disconnect by cancelling the asyncio.Task that wraps the view. The cancellation raises asyncio.CancelledError only at the next await point that yields control back to the event loop. While a synchronous call is executing, the event loop is blocked, so the CancelledError is not raised until that call returns and the view reaches an await.
task.cancel() on the view’s task.asyncio.CancelledError propagates exactly like any other exception: it is raised when the cancelled task resumes at an await.time.sleep, a pure‑Python CPU‑bound loop, or a database call that does not yield) prevents the event loop from checking for cancellation, delaying the exception until the block finishes.asyncio.wait_for does not alter how server‑initiated cancellation is delivered; it merely adds a timeout‑based source of CancelledError that behaves the same way.sync_to_async run in a separate thread. Cancelling the awaiting task stops the await but does not terminate the thread; the thread continues until the synchronous function completes.async def my_view(request):
lock = await acquire_lock()
try:
# first await point – cancellation can be observed here
await some_async_io()
# blocking work – move to a thread if it may be long
result = await sync_to_async(expensive_computation, thread_sensitive=True)
# second await point – cancellation can be observed here
await another_async_io()
finally:
# runs on normal return, on CancelledError, or on any other exception
await release_lock(lock)
# any other resource cleanup (close files, rollback txn, etc.)
If you are using an ASGI server other than the mainstream ones (Uvicorn/Daphne/Hypercorn), confirm that it implements the ASGI spec’s http.request lifecycle by cancelling the task on disconnect. The behaviour described above is required by the spec, so any compliant server will match it.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.