Does cancelling an async Django view roll back ORM work running via sync_to_async?
0 reputation · 20 Jan 2021, 15:27 UTC
0 reputation · 20 Jan 2021, 15:27 UTC
Django's ASGI handler cancels an async view's coroutine when the client disconnects, raising asyncio.CancelledError at the next await point. Cleanup in async views is therefore expected to happen in try/finally blocks that also account for CancelledError inheriting from BaseException since Python 3.8.
The unresolved behavior involves the ORM. Database calls made from an async view are synchronous and run through sync_to_async with thread_sensitive=True by default. When the surrounding coroutine is cancelled, it is unclear whether a query or transaction already executing in that thread is interrupted, and whether Django rolls back an open transaction automatically. The concern is that a cancelled view could still leave partial writes committed, with details possibly varying across ASGI servers and Django versions.
Open questions:
sync_to_async ORM work, or does that work continue to completion?atomic block to guarantee no partial commit?CancelledError from ordinary exceptions?34775 reputation · 20 Jan 2021, 20:55 UTC
No, cancelling an async Django view does not automatically roll back ORM work running via sync_to_async. If a client disconnects, the ASGI handler raises asyncio.CancelledError at the next await point in the coroutine, but this cancellation does not propagate into the separate thread where the synchronous ORM code is executing.
When you use sync_to_async, Django offloads the synchronous database operation to a thread pool. The relationship between the async view and the worker thread is as follows:
await point when the CancelledError is raised.To prevent partial writes during a disconnect, you cannot rely on the async view's lifecycle. You must define the transaction boundary within the synchronous code itself.
Wrap the database logic in transaction.atomic() inside the function being called by sync_to_async. This ensures that if an actual database error occurs, the work is rolled back. However, note that since the thread continues to run even after the view is cancelled, the transaction will still commit unless the synchronous code itself encounters an exception.
from django.db import transaction
from asgiref.sync import sync_to_async
def update_user_profile(user_id, data):
with transaction.atomic():
# All ORM work here is treated as a single unit
user = User.objects.select_for_update().get(id=user_id)
user.profile_data = data
user.save()
async def my_async_view(request):
try:
await sync_to_async(update_user_profile)(request.user.id, request.POST)
except asyncio.CancelledError:
# The thread is still running update_user_profile,
# but we can handle the cleanup of the async context here.
raise
To verify this behavior in your environment (assuming Django 3.1+ and an ASGI server like Daphne or Uvicorn):
sync_to_async function containing a time.sleep(10) followed by a database write.Diagnostic Detail: Are you using a custom thread executor or a non-standard database backend? Certain specialized executors may handle thread interruption differently than the default asgiref implementation.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.