Is it possible to use transaction.atomic with async functions?

I’m trying to use select_for_update in an async application. It requires a transaction to work, but I can’t find a way to use transaction.atomic in an async function. I’ve also tried to remove the async from the function and wrap all the calls it makes to other async functions using async_to_sync, but it hangs when calling model.save() which by the way also uses a version of sync_to_async as a decorator.

Any ideas?

EDIT: I’m using Django 3.1.8

How exactly are you trying to do this? (Can you post the view?)

Logically, I see no reason why you wouldn’t be able to use an atomic transaction within an asynchronous view, provided everything has finished by the time the transaction needs to be committed. However, given the transitions involved in switching between sync and async methods, I’m not sure what the practical implications would be.

I’ve rolled back everything to async. By just adding the transaction.atomic I get this exception. It happens when entering te async function transfer_task decorated with the @transaction.atomic.

/Users/johnny/.pyenv/versions/quinto-messenger/bin/python /Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm/django_test_manage.py test quintomessenger.tests.services.test_transfer_service_integration.TransferServiceDatabaseIntegrationTestCase.test_locking_task_db_line_when_transferring /Users/johnny/Projects/QuintoAndar/quinto-messenger
Testing started at 14:13 ...
Creating test database for alias 'default'...
System check identified no issues (0 silenced).







Error
Traceback (most recent call last):
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/asgiref/sync.py", line 139, in __call__
    return call_result.result()
  File "/Users/johnny/.pyenv/versions/3.8.6/lib/python3.8/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/Users/johnny/.pyenv/versions/3.8.6/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/asgiref/sync.py", line 204, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "/Users/johnny/Projects/QuintoAndar/quinto-messenger/quintomessenger/tests/services/test_transfer_service_integration.py", line 112, in test_locking_task_db_line_when_transferring
    new_task = await self.transfer_service.transfer_task(
  File "/Users/johnny/.pyenv/versions/3.8.6/lib/python3.8/contextlib.py", line 74, in inner
    with self._recreate_cm():
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/django/db/transaction.py", line 175, in __enter__
    if not connection.get_autocommit():
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/django/db/backends/base/base.py", line 389, in get_autocommit
    self.ensure_connection()
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
    raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

Destroying test database for alias 'default'...


Process finished with exit code 1

I suspect transaction.atomic doesn’t support async.
This is what happens if I try to call it as async with transaction.atomic:

/Users/johnny/.pyenv/versions/quinto-messenger/bin/python /Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm/django_test_manage.py test quintomessenger.tests.services.test_transfer_service_integration.TransferServiceDatabaseIntegrationTestCase.test_locking_task_db_line_when_transferring /Users/johnny/Projects/QuintoAndar/quinto-messenger
Testing started at 14:22 ...
Creating test database for alias 'default'...
System check identified no issues (0 silenced).







Error
Traceback (most recent call last):
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/asgiref/sync.py", line 139, in __call__
    return call_result.result()
  File "/Users/johnny/.pyenv/versions/3.8.6/lib/python3.8/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/Users/johnny/.pyenv/versions/3.8.6/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "/Users/johnny/.pyenv/versions/quinto-messenger/lib/python3.8/site-packages/asgiref/sync.py", line 204, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "/Users/johnny/Projects/QuintoAndar/quinto-messenger/quintomessenger/tests/services/test_transfer_service_integration.py", line 112, in test_locking_task_db_line_when_transferring
    new_task = await self.transfer_service.transfer_task(
  File "/Users/johnny/Projects/QuintoAndar/quinto-messenger/quintomessenger/core/services/transfer_service.py", line 32, in transfer_task
    async with transaction.atomic():
AttributeError: __aexit__

Destroying test database for alias 'default'...


Process finished with exit code 1

What if you tried something along the lines of:

@transaction.atomic()
def sync_view(request, ...):
    async_to_sync(some_async_code)(...)

Or, inverting that:

async def async_view(request, ...):
    sync_to_async(sync_db_transaction)(...)

@transaction.atomic()
def sync_db_transaction(...):
    ....

or even:

async def async_view(request, ...):
    await database_sync_to_async(sync_db_transaction)(...)

@transaction.atomic()
def sync_db_transaction(...):
    ....

or

def sync_db_transaction(...):
    with transaction.atomic():
        ...

???

I think the first option is the way to go. I’ve tried it already but got it hanging on the save method of the model which uses sync_to_async as a decorator when trying to turn it sync again with async_to_sync. I’ll implement this approach again but instead of using the same save method I’ll make one synchronous.