Обзор проблемы

Сервер DRF отправляет POST запрос вебхука на сервер приложения Django,
в процессе которого возникает следующая проблема при создании экземпляра модели Post.

  1. Тайм-аут ответа вебхука
  2. После создания объекта Post требуется время для обработки полей ManyToMany, таких как categories и tags
  3. Из-за этого ответ вебхука задерживается, и DRF интерпретирует это как ошибку

  4. Недостаточная целостность данных

  5. При немедленном вызове задачи Celery после обработки возникает проблема, когда выполняются команды tags.add(...), categories.add(...) до завершения этих операций
  6. В результате Celery обрабатывает неполные данные

  7. Получение пустых данных в Celery даже после on_commit()

  8. После обеспечения целостности данных с помощью on_commit() задача Celery назначается, но результаты запросов внутри задачи, такие как tags.all(), по-прежнему возвращают пустой список
  9. Это произошло потому, что Celery отправлял запросы на чтение в реплику БД, и существовали задержки в синхронизации master → replica

Стратегия решения

  1. Возврат ответа сразу после создания Post
  2. Создается только экземпляр Post, и сразу же возвращается ответ 202 Accepted, что позволяет избежать проблемы с тайм-аутом

  3. Обработка последующих задач в отдельном потоке

  4. Использовать threading.Thread(), чтобы разделить обработку отношений и вызов Celery от основного потока
  5. Разработать так, чтобы ответ вебхука не замедлялся

  6. Гарантировать транзакцию внутри post_process

  7. Объединить обработку полей ManyToMany в транзакцию с помощью transaction.atomic()
  8. После завершения операций назначить задачу Celery с помощью transaction.on_commit()
  9. Это гарантирует выполнение Celery после завершения обработки взаимосвязей

  10. Указать master DB для чтения внутри Celery

  11. Чтобы избежать проблем с целостностью из-за задержки реплики, явно использовать using('default') внутри задачи Celery
post = Post.objects.using('default').get(id=post_id)
tags = post.tags.using('default').all()
categories = post.categories.using('default').all()

Или использовать маршрутизатор БД для того, чтобы запросы Celery всегда обращались к master DB


Итоговая структура

  1. Сервер DRF → Запрос вебхука Django
  2. Django:
    • Немедленный возврат ответа после создания объекта Post
    • Последующие задачи выполняются в отдельном потоке
  3. Внутри потока:
    • Упорядочить отношения с помощью atomic()
    • Назначить задачу Celery с помощью on_commit()
  4. Celery:
    • Запрос данных из master DB (default)

Заключение

Все компоненты работают нормально по отдельности, но
для решения проблем с задержкой и целостностью данных, возникающих в распределенной архитектуре,
необходима была система проектирования, учитывающая не только структуру кода, но и поток данных, тайминг и задержки реплики БД.