← Назад к вопросам

Если у тебя есть вызов отложенной задачи Task.Delay, будет ли она выполняться в той же транзакции, в которой ее вызвали

1.8 Middle🔥 81 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отложенные задачи (Task.Delay) и транзакции БД

Типичный вопрос из C# опыта, но применим и к Python. Ключевой момент: отложенная задача НЕ выполняется в той же транзакции, где была запущена. Вот почему и как это работает.

Проблема транзакций с отложенными задачами

Когда ты вызываешь асинхронную задачу через Task.Delay (или asyncio.sleep в Python):

# Неправильно думать, что это выполняется в одной транзакции
async def create_order(order_data):
    async with db.begin():  # Открыли транзакцию
        db.add(Order(**order_data))
        await db.commit()  # Закрыли транзакцию
        
        # Эта задача вызывается ПОСЛЕ commit!
        asyncio.create_task(send_notification_email(order.id))

Почему НЕ в одной транзакции?

1. Транзакция заканчивается перед запуском фоновой задачи

async def process_payment(order_id: int):
    async with db.begin():  # Начало транзакции
        order = await db.get(Order, order_id)
        order.status = "paid"
        await db.commit()  # Конец транзакции ←
    
    # Здесь транзакция закрыта!
    # Следующий код выполняется вне транзакции
    await send_email(order.email)
    await update_analytics(order_id)

2. Фоновые задачи работают параллельно, не ждут завершения

async def create_user(user_data):
    async with db.begin():
        user = User(**user_data)
        db.add(user)
        await db.commit()  # Транзакция закрыта
    
    # Эта задача запускается async — не ждёт завершения
    asyncio.create_task(send_welcome_email(user.id))
    
    # Функция возвращает результат немедленно
    return {"user_id": user.id, "status": "created"}

Правильная архитектура

Вариант 1: Отложенные задачи через очередь (рекомендуется)

from celery import Celery
from sqlalchemy.orm import Session

celery_app = Celery('tasks', broker='redis://localhost')

@celery_app.task
def send_notification(user_id: int):
    # Эта задача имеет СОБСТВЕННУЮ сессию БД
    session = SessionLocal()
    try:
        user = session.query(User).filter(User.id == user_id).first()
        # Отправляем письмо
        send_email(user.email)
    finally:
        session.close()

async def create_order(order_data: dict):
    async with db.begin():
        order = Order(**order_data)
        db.add(order)
        await db.commit()  # Транзакция закрыта
    
    # Запускаем задачу в очередь (вне транзакции)
    send_notification.delay(order.id)
    return {"order_id": order.id}

Вариант 2: Фоновые задачи в FastAPI

from fastapi import FastAPI
from fastapi.background import BackgroundTasks

app = FastAPI()

def send_email(email: str):  # Синхронная функция
    # Имеет отдельный контекст — не в транзакции
    import smtplib
    # отправляем письмо
    pass

@app.post("/orders")
async def create_order(order_data: dict, background_tasks: BackgroundTasks):
    async with db.begin():  # Открыли транзакцию
        order = Order(**order_data)
        db.add(order)
        await db.commit()  # Закрыли транзакцию
    
    # Добавляем задачу в очередь — выполнится позже
    background_tasks.add_task(send_email, order.email)
    return {"order_id": order.id}

Вариант 3: Если нужна БД в фоновой задаче

async def process_with_db(user_id: int):
    # Создаём новую сессию для фоновой задачи
    async with AsyncSession() as session:
        user = await session.get(User, user_id)
        # Работаем с БД в отдельной транзакции
        await session.commit()

async def create_user(user_data):
    async with db.begin():
        user = User(**user_data)
        db.add(user)
        await db.commit()  # Одна транзакция
    
    # Создаём фоновую задачу с СОБСТВЕННОЙ транзакцией
    asyncio.create_task(process_with_db(user.id))

Риски игнорирования этого правила

1. Race condition — задача может читать несохранённые данные

# ❌ Опасно
user = User(name="John")
db.add(user)
db.flush()  # user.id существует, но не коммичено!

# Задача может прочитать данные до commit()
background_send_email(user.id)

db.commit()  # Если здесь ошибка — email был отправлен зря

2. Deadlock — транзакция блокирует задачу

# ❌ Если задача попытается обновить тот же объект
async with db.begin():
    user = await db.get(User, user_id, with_for_update=True)
    # Блокировка на уровне БД
    
    asyncio.create_task(update_user_preferences(user_id))
    # update_user_preferences попытается получить лок — дедлок!

3. Откат данных без отката сайд-эффектов

# ❌ Проблема
async with db.begin():
    order = Order(amount=100)
    db.add(order)
    await db.commit()
    
    send_receipt_email(order.id)  # Email отправлен
    
    # Если здесь ошибка, заказ откатится, но письмо уже отправлено
    raise Exception("Something went wrong")

Лучшие практики

Используй очередь (Celery, RQ, etc.) для критических фоновых задач

Коммитьте БД перед запуском фоновой задачи

Если фоновой задаче нужна БД — дай ей отдельную сессию

Логируй/мониторь фоновые задачи (они могут молча падать)

Используй идемпотентность — задача может выполниться дважды

Итог

Отложенные задачи (Task.Delay, asyncio.create_task) НЕ выполняются в той же транзакции, потому что:

  1. Транзакция закрывается перед их запуском
  2. Они работают в отдельном контексте (возможно, даже в отдельном процессе)
  3. Это сделано специально для безопасности и гибкости

Для правильного управления такими задачами используй очередь сообщений (Celery) или фоновые задачи в FastAPI.