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:
- Заказ переходит в
awaiting_confirm, записывается deadline. - Приходит подтверждение или истекает срок.
- Подтверждение до deadline в состоянии
awaiting_confirmразрешает одну отправку поorder42:ship. - Истечение срока переводит заказ в
timed_out. - 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 состояние не меняет.
Учебный конфиг проверки инвариантов
Ниже приведён условный вход для самопроверки дизайна. Измерения продукта не проводились.
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
Этот текст можно сохранить в текстовом редакторе и распечатать как чек-лист. Он фиксирует инварианты проекта и не является гарантией инструментов.
