Содержание
  1. Очередь задач и история исполнения
  2. Состояние бизнес-процесса и внешний idempotency key
  3. Заполненная матрица трёх сбоев order42
  4. Падение между внешним эффектом и подтверждением
  5. Ожидание confirm и timeout без автоотправки
  6. Границы Celery и Temporal и standalone activities
  7. Частые вопросы
  8. Учебный конфиг проверки инвариантов
Подборки и сравнения

Temporal или Celery: workflow и очередь задач

14 ноября 2026 · 9 минут чтения

Temporal или Celery: workflow и очередь задач — это выбор между двумя разными способами хранить ход работы. Celery доставляет сообщения воркерам, а состояние бизнес-процесса живёт в базе приложения. Temporal хранит историю исполнения Workflow и восстанавливает её при Replay. Ниже — сравнение на бумажном примере заказа order42.

Очередь задач и история исполнения

Документация Celery описывает механику так: очереди задач распределяют работу между потоками и машинами, клиент кладёт сообщение в очередь, брокер передаёт его воркеру. Система собирается из нескольких воркеров и брокеров, что даёт горизонтальное масштабирование и высокую доступность. Задача — единица работы; результат и его состояние Celery хранит отдельно от бизнес-данных приложения.

Документация Temporal описывает другой слой. У каждого исполнения Workflow есть эксклюзивный доступ к своему локальному состоянию. При Replay сгенерированные команды сверяются с уже существующей Event History. История — это журнал завершённых событий, и по нему движок восстанавливает позицию процесса после перезапуска воркера.

Разница проявляется в том, где лежит «память» процесса. В Celery её пишет приложение: таблица заказов, поле этапа, отметка времени. В Temporal её ведёт движок: события Workflow, таймеры, сигналы. Обе схемы требуют, чтобы приложение знало свой бизнес-смысл. Ни одна из них не превращает внешний платёж в exactly-once операцию.

Для order42 это значит: цепочка «списать → ждать подтверждения → отправить» описывается в коде в обоих случаях. В Celery вы сами создаёте таблицу состояний и переходы. В Temporal вы описываете Workflow, а движок хранит его историю и переигрывает её. Выбор здесь — про то, кто отвечает за восстановление позиции после падения.

Практический признак: если задача живёт секунды и не имеет внутренних ожиданий, разница мала. Если процесс живёт часы и внутри есть пауза на человека, вопрос хранения состояния становится главным. Документация Temporal отдельно упоминает таймеры: код Workflow может ожидать срабатывания таймера через awaitable. Сам запуск таймера не обязывает весь Workflow ждать только его результата.

Состояние бизнес-процесса и внешний idempotency key

В учебном примере order42 внешние системы принимают idempotency key, а статус операции можно сверить запросом. Ключи: order42:charge для платежа и order42:ship для отправки. Это допущение учебного контракта внешних систем. Поддержку такого контракта проверяют отдельно.

Эталон проекта: charge_count=1; до подтверждения ship_count=0; после своевременного подтверждения в состоянии awaiting_confirm — ship_count=1; timeout 24 часа сам по себе отправку не вызывает.

В Celery состояние процесса — явный проект приложения. База хранит order_id, этап и признак подтверждения. Повторная задача проверяет эти поля и внешний статус, а запрос оплаты идёт с тем же ключом order42:charge. Документация Celery подтверждает, что система поддерживает retry, состояния задач, chains и композицию; долгий бизнес-процесс и внешнюю идемпотентность проектирует приложение.

В Temporal Activity может повториться, поэтому тот же внешний ключ обязателен. Документация Temporal рекомендует делать Activity идемпотентной, чтобы повторы обрабатывались без дублирующих побочных эффектов. EventHistory хранит завершённые события Workflow, и запись истории не заменяет платёжную идемпотентность.

Ключевая мысль: idempotency key — это контракт с внешней системой. Он живёт на стороне платёжного шлюза и службы доставки. Ни Celery, ни Temporal не отменяют необходимость передавать его в каждом запросе.

Для order42 это даёт простую проверку: перед любым списанием приложение сверяет, не было ли уже успешного order42:charge. Перед отправкой — что подтверждение принято и заказ всё ещё в awaiting_confirm. Эти проверки пишет разработчик, и они одинаково нужны в обеих архитектурах.

Заполненная матрица трёх сбоев order42

Матрица ниже описывает бумажный дизайн. Системы по нему не запускались. Значения — ожидаемые инварианты проекта при заданном внешнем контракте.

Сбой Celery: где состояние Temporal: где состояние Ожидание
СБОЙ1: дубликат сообщения или повтор задачи DB приложения хранит order_id и этап; повторный task сверяет их и внешний статус Activity может повториться; EventHistory хранит завершённые события Workflow списание 1, отправка 0 до confirm
СБОЙ2: воркер упал после успешного списания, до сохранения подтверждения по order42:charge приложение запрашивает внешний payment status и восстанавливает payment_ref в своей DB если completion Activity не записан в History, Activity может быть повторена; восстановление идёт по тому же ключу и status lookup списание 1, отправка 0
СБОЙ3: ожидание человека до 24 часов DB хранит awaiting_confirm и deadline; периодический checker выбирает просроченные записи и ставит задачи состояние Workflow и durable timer восстанавливаются из EventHistory; Signal подтверждения разрешает Shipping Activity timeout переводит заказ в timed_out и не запускает ship

В обеих схемах timeout переводит заказ в timed_out и не запускает отправку. Подтверждение, принятое до deadline в состоянии awaiting_confirm, разрешает ровно одну отправку по ключу order42:ship. Поздний или повторный confirm после timed_out терминальное состояние не меняет и отправку не запускает — это выбранная бизнес-политика учебного проекта.

Матрица показывает главное различие: в Celery расписание и его устойчивость надо реализовать и настроить, в Temporal durable timer восстанавливается из истории. Оба варианта требуют явного правила «что считать подтверждением».

Падение между внешним эффектом и подтверждением

СБОЙ2 — самый неприятный случай. Внешний эффект уже произошёл, а внутренняя запись о нём отсутствует. Это классическая проблема неизвестного исхода.

В Celery приложение запрашивает внешний payment status по ключу order42:charge и восстанавливает payment_ref в своей базе. Повторный запрос с тем же ключом новой оплаты не создаёт. Здесь нужен отдельный код сверки, и он должен быть идемпотентным.

В Temporal, если completion Activity не записан в History, Activity может быть повторена. Восстановление внешнего результата использует тот же ключ и status lookup. Документация Temporal уточняет: каждая попытка Activity стартует из начального состояния, если код не использует Heartbeat detail payload для чекпоинтов. Запись истории не заменяет платёжную идемпотентность.

Общий вывод для order42: безопасность держится на внешнем контракте. Платёжный шлюз должен уметь ответить на вопрос «был ли платёж с ключом order42:charge». Если предусмотренная этим учебным дизайном сверка недоступна, результат списания остаётся для него неизвестным. Для реальной интеграции нужен отдельный подтверждённый способ восстановления результата по контракту платёжного провайдера.

Проверка результата: после сбоя charge_count остаётся равным 1, ship_count — 0. Если при повторной попытке появляется второе списание, проверьте передачу ключа и соблюдение контракта идемпотентности. Причину нарушения нужно установить по журналам и условиям платёжного провайдера. Это ошибка интеграции, и она воспроизводится в обеих системах.

Ожидание confirm и timeout без автоотправки

Ожидание человека до 24 часов — это долгая пауза внутри процесса. В Celery её реализует приложение: база хранит awaiting_confirm и deadline, отдельный периодический checker выбирает просроченные записи и ставит задачи, подтверждение обновляет базу. Расписание и его устойчивость надо реализовать и настроить.

В Temporal состояние Workflow и durable timer восстанавливаются из EventHistory. Signal подтверждения разрешает Shipping Activity только если confirm принят до deadline и заказ ещё в awaiting_confirm. Документация Temporal описывает возможность ожидать срабатывания таймера. В этом проекте код Workflow должен обработать подтверждение и истечение срока согласно заданной бизнес-политике.

Ключевое правило проекта: timeout переводит заказ в timed_out и не запускает ship. Автоотправка по истечении срока не происходит. Эту бизнес-политику требуется явно реализовать в приложении. Автоматической гарантии со стороны инструмента здесь нет.

Практический порядок действий для order42:

  1. Заказ переходит в awaiting_confirm, записывается deadline.
  2. Приходит подтверждение или истекает срок.
  3. Подтверждение до deadline в состоянии awaiting_confirm разрешает одну отправку по order42:ship.
  4. Истечение срока переводит заказ в timed_out.
  5. Confirm после timed_out состояние не меняет.

В Celery шаг 4 выполняет checker по расписанию. В Temporal его выполняет durable timer. Логика решения одинакова, различается место хранения таймера.

Границы Celery и Temporal и standalone activities

Сравнение не делит инструменты на «только простые» и «только многошаговые». Celery поддерживает retry, состояния задач, chains и композицию. Temporal поддерживает standalone activities — Activity, которую запускают напрямую из Temporal Client без обёртки в Workflow, как указано в документации проекта.

Документация Temporal по миграции с Celery отмечает: если приложение использует Canvas-примитивы — chain, group, chord, — логика координации должна где-то жить надёжно. Для одного действия подходит standalone activity, для многошагового процесса — Workflow.

Ещё одно различие из той же документации: Celery считает retry после первой попытки, Temporal считает общие попытки. Это влияет на настройку лимитов и на чтение логов.

Учебный пример order42 сравнивает место хранения и восстановления трёх состояний. Он не измеряет скорость, не исполняет платежи и не обещает exactly-once внешнего действия. Компенсация требует кода автора; автоматического отката через произвольные сервисы нет.

Практический выбор: короткие фоновые задачи без внутренних ожиданий укладываются в очередь Celery. Долгий процесс с паузами, сигналами и восстановлением позиции выигрывает от истории Workflow. Оба варианта требуют внешних idempotency key и сверки статуса.

Частые вопросы

Temporal или Celery: что выбрать для коротких фоновых задач?
Для задач на секунды без внутренних ожиданий подходит Celery: очередь доставляет сообщение воркеру, а состояние результата хранится отдельно. Temporal тоже умеет запускать одиночные действия через standalone activities. Решение зависит от того, нужна ли история исполнения и восстановление позиции.

Как хранить состояние долгого бизнес-процесса в Celery?
Состояние проектирует приложение: база хранит order_id, этап, awaiting_confirm и deadline. Периодический checker выбирает просроченные записи и ставит задачи. Расписание и его устойчивость надо реализовать и настроить.

Зачем нужен внешний idempotency key?
Ключ защищает от повторного списания при повторе задачи или Activity. В примере order42 это order42:charge и order42:ship. Документация Temporal рекомендует делать Activity идемпотентной, чтобы повторы не давали дублирующих побочных эффектов.

Что происходит при падении воркера после успешного списания?
Внешний эффект уже произошёл, а внутренняя запись отсутствует. Приложение запрашивает внешний payment status по ключу и восстанавливает payment_ref. Повтор с тем же ключом новой оплаты не создаёт.

Запускает ли timeout автоматическую отправку?
Нет. Timeout переводит заказ в timed_out и отправку не запускает. Подтверждение до deadline в состоянии awaiting_confirm разрешает ровно одну отправку по order42:ship. Поздний confirm состояние не меняет.

Учебный конфиг проверки инвариантов

Ниже приведён условный вход для самопроверки дизайна. Измерения продукта не проводились.

ТЕКСТ19 строк
order42 invariants:
  charge_key = order42:charge
  ship_key   = order42:ship
  states     = created -> awaiting_confirm -> confirmed | timed_out
  deadline   = 24h

check before charge:
  if external_status(charge_key) == succeeded: reuse payment_ref
  else: charge(charge_key)

check before ship:
  if state == awaiting_confirm and confirm_at <= deadline: ship(ship_key)
  else: skip

expected:
  charge_count = 1
  ship_count   = 0 before confirm
  ship_count   = 1 after timely confirm
  timeout alone does not ship

Этот текст можно сохранить в текстовом редакторе и распечатать как чек-лист. Он фиксирует инварианты проекта и не является гарантией инструментов.

Читайте также

3 материала