Как использовать транзакцию с функциями "async" в Django?

Когда async def call_test(request): вызывается async def test(): как показано ниже (я использую Django==3.1.7):

async def test():
    for _ in range(0, 3):
        print("Test")

async def call_test(request):

    await test() # Here

    return HttpResponse("Call_test")

Ошибки нет, на консоли отображается правильный результат:

Test
Test
Test

Но, когда я накладываю @transaction.atomic() на async def test():, как показано ниже:

@transaction.atomic() # Here
async def test():
    for _ in range(0, 3):
        print("Test")

# ...

Произошла следующая ошибка:

django.core.exceptions.SynchronousOnlyOperation: Вы не можете вызвать это из асинхронного контекста - используйте поток или sync_to_async.

Итак, я поместил @sync_to_async на @transaction.atomic(), как показано ниже:

@sync_to_async # Here
@transaction.atomic()
async def test():
    for _ in range(0, 3):
        print("Test")

# ...

Но произошла другая ошибка, описанная ниже:

RuntimeWarning: coroutine 'test' was never awaited handle = None # Необходим для разрыва циклов при возникновении исключения. RuntimeWarning: Включите tracemalloc для получения трассировки выделения объекта

Итак, как я могу использовать транзакцию с async функциями в Django?

Я нашел в документации Django 4.1 написано следующее:

Транзакции пока не работают в асинхронном режиме. Если у вас есть часть кода, которой требуется поведение транзакций, мы рекомендуем вам написать эту часть как отдельную синхронную функцию и вызвать ее с помощью sync_to_async().

Так, @transaction.atomic() нельзя использовать с функциями async в версии порядка Django 3.1.7, а также

У меня была похожая проблема. Разница была в том, что мне нужно было использовать атомарную транзакцию в качестве асинхронного менеджера контекста. Прочитав, что несколько человек писали, что это невозможно в текущей версии Django 4.2.4. Начал самостоятельно исследовать кодовую базу

Первое, что я заметил, transaction.atomic() не реализует логику самостоятельно. Он просто возвращает декоратор контекста Atomic. У этого декоратора есть 3 метода, которые важно обработать:

  • __init__ atomic() передает ему аргументы, поэтому мы должны их обработать.
  • __enter__ и __exit__, которые важны для работы контекстного менеджера.
class Atomic(ContextDecorator):
    ...
    def __init__(self, using, savepoint, durable):
    ...
    def __enter__(self):
    ...
    def __exit__(self, exc_type, exc_value, traceback):
    ...

def atomic(using=None, savepoint=True, durable=False):
    # Bare decorator: @atomic -- although the first argument is called
    # `using`, it's actually the function being decorated.
    if callable(using):
        return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
    # Decorator: @atomic(...) or context manager: with atomic(...): ...
    else:
        return Atomic(using, savepoint, durable)

Для работы асинхронных менеджеров контекста нам необходимо реализовать __aenter__ и __aexit__. Исходя из этого, я придумал рабочее решение:

from django.db.transaction import Atomic
from asgiref.sync import sync_to_async

class AsyncAtomicContextManager(Atomic):
    def __init__(self, using=None, savepoint=True, durable=False):
        super().__init__(using, savepoint, durable)

    async def __aenter__(self):
        await sync_to_async(super().__enter__)()
        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        await sync_to_async(super().__exit__)(exc_type, exc_value, traceback)

Который можно использовать как стандартный асинхронный менеджер контекста:

async def test():
    async with AsyncAtomicContextManager():
        ...

Тогда вы можете сделать декоратор из этого:

def aatomic(fun, *args, **kwargs):
    async def wrapper():
        async with AsyncAtomicContextManager():
            await fun(*args, **kwargs)
    return wrapper

@aatomic
async def test():
    ...
Вернуться на верх