Код-ревью Middle Celery и очереди спрашивают часто обновлено

Код-ревью задачи Celery

Вопрос

Найдите проблемы в задаче начисления бонусов и месте её вызова.

from celery import shared_task
from django.db import transaction
from .models import Order, Bonus


@shared_task(autoretry_for=(Exception,), max_retries=None)
def accrue_bonus(order):
    Bonus.objects.create(
        user_id=order.user_id,
        amount=order.total // 10,
    )
    notify_user(order.user_id)


def checkout(cart):
    with transaction.atomic():
        order = Order.objects.create(user=cart.user, total=cart.total)
        accrue_bonus.delay(order)
        cart.items.all().delete()
    return order

Коротко

Исправленный вариант: Для строгой гарантии у модели Bonus стоит уникальное ограничение на orderid.

Что должен найти ревьюер

  1. Ошибкастрока 6
    Ретраи на любое исключение без ограничения попыток и без задержки: баг в коде будет повторяться бесконечно
  2. Ошибкастрока 7
    В задачу передаётся ORM-объект: с JSON не сериализуется, с pickle устареет; передавать order.id
  3. Ошибкастрока 8
    Задача не идемпотентна: при ретрае бонус начислится повторно; нужна уникальность по заказу (get_or_create или уникальный индекс)
  4. Ошибкастрока 18
    Задача ставится до коммита: воркер может не найти заказ, а при откате бонус начислится за несуществующий заказ; transaction.on_commit

Разбор

Исправленный вариант:

@shared_task(autoretry_for=(ConnectionError, TimeoutError), retry_backoff=True,
             retry_jitter=True, max_retries=5, acks_late=True)
def accrue_bonus(order_id: int) -> None:
    order = Order.objects.get(pk=order_id)
    bonus, created = Bonus.objects.get_or_create(
        order_id=order.id,
        defaults={'user_id': order.user_id, 'amount': order.total // 10},
    )
    if created:
        notify_user(order.user_id)


def checkout(cart):
    with transaction.atomic():
        order = Order.objects.create(user=cart.user, total=cart.total)
        cart.items.all().delete()
        transaction.on_commit(lambda: accrue_bonus.delay(order.id))
    return order

Для строгой гарантии у модели Bonus стоит уникальное ограничение на order_id.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.