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.
1 Like