Для координации многошаговой работы между сервисами BloodGPT мы используем инструмент уровня workflow engine, конкретно inngest — не message broker (сырой транспорт) и не job queue (fire-and-forget «выполни метод»). Это и каркас pipeline’ов (recognize → … → interpret → enrichments → PDF), и целевой substrate для межсервисной координации / очередей / retry / concurrency (вместо ad-hoc Python-сервисов с самодельными Redis-очередями — см. no-self-rolled-queues, loinc-unification-direction). Legacy .NET-бэкенд работал на уровне job queue (Hangfire-style background jobs); новый стек поднялся на уровень workflow engine. Внутри этого уровня: Temporal — избыточен для нашего профиля; Vercel WDK — beta, не адоптирован; job queues (Hangfire / BullMQ / Celery) — это уровень ниже, не наш.
Контекст
Вопрос — на каком уровне инструмента координировать многошаговую работу между сервисами: pipeline’ы recognize → normalize → LOINC → ranges → save-FHIR → interpret → enrichments → PDF, плюс cross-service вызовы / очереди / retry, которые сейчас в Python-сервисах сделаны руками (Async API + Redis BZPOPMIN + worker pod + job polling). Для этого есть три уровня инструментов — их легко спутать, а разница принципиальная.
Три уровня инструментов для межсервисной координации
| Уровень | Примеры | «Что делает» |
|---|---|---|
| Message broker | RabbitMQ, Kafka, SQS, NATS | «доставь сообщение». Сырой транспорт. Не знает про jobs / retry / scheduling. |
| Job queue | Hangfire (.NET), Sidekiq (Ruby), Celery (Python), BullMQ (Node) | «выполни метод надёжно». Сериализует вызов (тип+метод+аргументы) → хранилище → worker. Fire-and-forget. |
| Workflow engine | Temporal, Inngest, Vercel WDK, Step Functions, Restate | «смоделируй и управляй бизнес-процессом как кодом» — оркестрация, long-running, реакция на события, queryable state. |
Мы выбираем верхний уровень — workflow engine (broker и job queue для нашей задачи недостаточны: первый — только транспорт, второй — только надёжное выполнение метода без оркестрации, ожидания, состояния), а внутри него — Inngest.
Workflow engine — принципиально другая модель, чем «job queue, которая видит шаги»:
- Оркестрация — if/else, циклы, параллельные ветки, sub-workflows записываются как обычный код.
- Long-running state — workflow живёт дни/недели; ждёт approval, таймер, событие. Job выполнился и исчез.
- Реакция на события —
waitForEvent("user.approved"); workflow спит и просыпается от сигнала. Job ждать не умеет. - Запрашиваемое состояние — «где сейчас этот заказ / анализ?» в любой момент. С job queue это строишь сам.
- Компенсация (saga) — шаг 5 упал → откатить 1–4. Job queue не знает про связи между jobs.
Суть workflow engine — «виртуализация исполнения»: пишешь код как обычную программу (последовательные вызовы, if/else, циклы), а движок прячет за этим распределённую систему — шаги на разных машинах, переживают crash’и, переживают деплой, ждут неделями. Temporal так это и называет — «Durable Execution virtualizes execution…». Сам термин «durable execution» — маркетинговый (2019, Temporal); технику строили те же люди (Fateev + Abbas) с 2002: Amazon internal → SWF (2009) → Microsoft Durable Task Framework (2014) → Uber Cadence (2015) → Temporal (2019); Inngest/Restate/Vercel подхватили термин позже. Ключевое: workflow engine ≠ улучшенная очередь — job queue работает на уровне сообщений, workflow engine на уровне процессов.
Наш профиль: data pipeline, не side-effect process
Два типа процессов:
- Side-effect process — шаги меняют внешний мир (списывают деньги, бронируют, создают записи в чужих системах). Поздний шаг упал → ранние надо откатить → нужна saga. Пример: booking (reserve hotel → book flight → charge card → FAIL → cancel flight → cancel hotel).
- Data pipeline — шаги трансформируют данные, не меняя внешний мир; результат шага = input следующего. Шаг упал → просто retry, предыдущие результаты валидны, откатывать нечего. Пример: AI-анализ (upload → recognize → validate → interpret → FAIL → just retry interpret).
BloodGPT-пайплайн — data pipeline. recognize → normalize → LOINC → ranges → save-FHIR → interpret → enrichments → PDF — это трансформация данных. FHIR-запись (Healthcare API) — наш собственный store, не «чужая система, которую надо откатывать»; единственный реальный внешний side-effect — webhook клиенту, и он в самом конце. Что нам поэтому нужно:
| нужно нам? | |
|---|---|
| Crash recovery | Да — не перезапускать дорогие LLM-вызовы при крэше/деплое |
| HITL / long-running | Да — doctor review (approve/edit/reject до доставки пациенту) |
| Queryable state | Полезно — «где сейчас анализ X?» (UI / monitoring / DQD) |
| Compensation / saga | Нет — data pipeline, откатывать нечего |
Это сразу обесценивает главное, чем Temporal отделяется от остальных (нативные saga + queryable-state-из-кода): saga нам не нужна, а queryable-state можно собрать из FHIR-ресурсов (что записано) + Inngest UI (где выполнение).
Рассматривали
Inngest — HTTP re-invocation + memoization (выбран)
inngest: workflow engine, который создаёт «иллюзию единого процесса». Crash recovery — через re-invoke + memoization: при крэше функция вызывается заново, step.run("id", fn) уже выполненных шагов возвращают результат из state store (не выполняются повторно). Две модели подключения: serve() (push-HTTP, serverless) и connect() (pull-WebSocket — для long-running K8s/ECS-сервисов, без публичного URL, latency как у Temporal). Деплой: single binary (SQLite по умолчанию / PostgreSQL+Redis для prod) или Cloud (managed). Flow control сильнее, чем у Temporal — built-in throttle / concurrency (per-function, keyed) / priority. HITL — waitForEvent (до 7 дней, matching по полям). Минусы: saga нет (руками через try/catch — нам не нужно); queryable-state — только Dashboard + SQL Insights, нет программного API; self-hosted в beta.
Temporal — event sourcing + replay (оверкилл для нас)
Workflow engine эталонного уровня: crash recovery / HITL / queryable-state / saga — все четыре production-proven. Worker (ваш long-running процесс) poll’ит Temporal Server (Temporal не вызывает ваш код); при крэше — replay из Event History (activities не пере-вызываются, SDK подставляет результаты). Multi-language (Go/Java/Python/TS/.NET). Цена — инфраструктура: 4 независимо масштабируемых сервиса (Frontend / History / Matching / Worker) + Cassandra/PostgreSQL/MySQL, History Shards. Нативный saga.addCompensation() с авто-rollback в обратном порядке и QueryWorkflow() из кода — это то, что реально отделяет Temporal; для data pipeline без side-effects обе вещи не нужны. Flow control (throttle/priority) — слабее, чем у Inngest. Вывод: переплата за надёжность side-effect-процессов, которых у нас нет.
Vercel WDK — compiler + sandboxed VM + Worlds (beta, не адоптирован)
SWC-plugin трансформирует один файл в три output’а (Client / Step / Workflow mode); workflow-код бежит в sandboxed VM (нет прямого Node.js, нет Date()/Math.random(), sleep() вместо setTimeout, всё I/O через steps); replay — event sourcing, как у Temporal, но без отдельного сервера (всё в вашем приложении + «World» для persistence — Local/Vercel/self-host/custom). DX лучший (time-travel debugging, директивы "use step", портируемость через Worlds). Но beta: crash recovery работает, HITL/long-running сырое (есть баги), queryable-state — только Workbench для dev, нет concurrency. TypeScript-only (наш стек — плюс), но незрелость перевешивает.
Job queues (Hangfire / BullMQ / Celery / Sidekiq) — другой класс, не наш
«Выполни метод надёжно», fire-and-forget. Нет long-running state, нет оркестрации (только цепочки ContinueWith), нет реакции на события, нет компенсации, нет shared state между jobs. Legacy .NET-бэкенд BloodGPT был на этом уровне (background jobs + ручной retry/timeout — отсюда же и таймаут+перезапуск для recognition-зацикливаний, см. gemini-doom-loop); Python normalization/LOINC-сервисы свернули свой Redis-job-queue (Async API + BZPOPMIN + worker pod + job polling) — это всё job-queue-tier механизмы, миграционные цели на Inngest (см. no-self-rolled-queues, loinc-unification-direction).
Сводка
| Temporal | Inngest | Vercel WDK | Hangfire (job queue) | |
|---|---|---|---|---|
| Категория | workflow engine | workflow engine | workflow engine | job queue |
| Deployment | 4 сервиса + DB (Cassandra/PG) | single binary (SQLite/PG+Redis) или Cloud | build plugin + World | NuGet + SQL Server |
| Процесс / вызов | long-running worker, poll’ит сервер | HTTP endpoint (push) или WS (pull, connect) | sandboxed VM, встроен в app | background thread, poll’ит БД |
| При crash | replay из Event History | re-invoke + memoize | replay из events | retry job с начала |
| Crash recovery | +++ proven | +++ proven | + работает (beta) | — (нет checkpoint’ов) |
| HITL / long-running | +++ Signal+wait, без лимита | ++ waitForEvent, до 7 дней | + sleep()+webhooks, beta+баги | — |
| Queryable state | +++ из кода + REST + UI | + Dashboard + SQL Insights, нет API | − только dev Workbench | − |
| Saga / compensation | +++ нативно, авто-rollback | − руками (нам не нужно) | − руками | − |
| Flow control (throttle/concurrency/priority) | base (task-queue limits) | built-in, сильнее всех | нет | queue-level |
| Языки | Go/Java/Python/TS/.NET | TS/JS/Python/Go | TS/JS | C# |
Выбрали
Уровень — workflow engine (а не message broker / job queue); движок — Inngest. Каркас pipeline’ов BloodGPT и целевой substrate для межсервисной координации / очередей / retry / concurrency (вместо ad-hoc Python-сервисов с самодельными Redis-очередями). Self-hosted локально (через Tilt), Cloud (платный план) в проде; модель подключения воркеров — вероятно connect() (K8s, long-running), TBD verify.
Почему
- Нужен именно workflow-engine уровень. Нам нужны оркестрация (последовательность шагов как код, if/else, параллельные ветки), long-running (doctor review — ждать approval дни), реакция на события, queryable state («где анализ X?»), crash-recovery. Message broker даёт только транспорт; job queue — только «выполни метод надёжно», без оркестрации / ожидания / состояния / связей между jobs. Дальше — почему Inngest внутри этого уровня.
- Мы data pipeline → saga не нужна, а это главное преимущество Temporal; значит его тяжёлая инфра (4 сервиса + Cassandra/PG, History Shards) — переплата.
- TS-native — наш стек на TypeScript; Temporal-овский multi-language — фича, которой мы не пользуемся; WDK тоже TS, но beta с багами.
- Минимальная инфра — single binary, SQLite локально / PostgreSQL+Redis в проде (или вообще Cloud) vs Temporal-кластер.
- Лучший flow control — built-in throttle / concurrency (keyed) / priority; критично для per-tenant rate limiting и noisy-neighbor protection (
concurrency: { key: "event.data.organizationId", limit: N }— см. inngest § Multi-tenant паттерн). connect()— pull через persistent WebSocket для long-running сервисов в K8s (наши воркеры): не нужен публичный URL, latency как у Temporal, при этом простой API Inngest.- «Иллюзия единого процесса» + crash recovery (re-invoke + memoize) — закрывает главную потребность data pipeline: не пере-запускать дорогие LLM-вызовы при крэше/деплое.
- HITL через
waitForEvent(до 7 дней, matching по полям) — хватает на цикл doctor review.
Следствия
- Оркестрация / очереди / retry / fan-out — через Inngest, не самодельные in-app механизмы (worker-pool, ручной retry-loop, рекурсивная concurrency): см. no-self-rolled-queues. Legacy .NET background-jobs и Python Redis-job-queues — это job-queue-tier, миграционные цели.
- Data pipeline ⇒ нет compensation-кода: если шаг упал — просто retry, предыдущие результаты валидны. Не пытаться «откатывать» FHIR-записи как side-effects (FHIR — наш store, не чужая система).
- Как именно компоновать наш пайплайн на Inngest (один большой оркестратор vs event-choreography vs гибрид) — отдельный открытый вопрос: inngest-pipeline-orchestration-vs-choreography.
- Большие данные между шагами — через FHIR-store, не через step output (лимиты Inngest: step output 4 MB, run state 32 MB; и медданные не должны оседать в логах/трейсах оркестратора) — см. inngest § Передача данных между шагами, phi-in-fhir-not-sql.
- Queryable state «где анализ X?» собирается из FHIR-ресурсов (что записано) + Inngest UI (где выполнение) — программного API статуса у Inngest нет, своего «pipeline-run»-объекта с прогрессом мы пока не делаем (см. open в inngest-pipeline-orchestration-vs-choreography).
- Inngest Cloud — платный план (тариф TBD verify); локально — self-hosted (single binary через Tilt). Переход на self-hosted в проде — не зафиксирован (self-hosted beta).
- Checkpointing (Inngest dev preview, Dec 2025) — снизит latency длинных
step.run-цепочек (~−50% inter-step), делает большой оркестратор дешевле; следить.
Открытые вопросы
- Inngest Cloud vs self-hosted в проде — сейчас Cloud; можно ли / стоит ли перейти на self-hosted (single binary beta, нужен HA-setup) — не решено.
connect()vsserve()— какую модель реально используют наши воркеры (analysis-worker, fhir-services) — вероятноconnect(), TBD verify.- Если появятся side-effect-процессы (например, что-то с внешними платёжными / insurance API, где нужен откат) — Inngest saga не даёт; тогда либо отдельный движок (Temporal-уровень) для тех процессов, либо saga руками. Пока не актуально — фиксируем как «если понадобится».
Связано
- inngest — vendor-страница: как устроен Inngest и как мы его используем (function/step, step.run/invoke/sendEvent, limits, deployment, pricing, roadmap)
- no-self-rolled-queues — оркестрация / очереди / concurrency / retry — через Inngest, не самодельные in-app механизмы (job-queue-tier механизмы внутри/вместо workflow engine — анти-паттерн)
- inngest-pipeline-orchestration-vs-choreography — как именно компоновать наш пайплайн на Inngest (status: draft)
- recognition-enrichment-hourglass — наш пайплайн как data pipeline (песочные часы: unstructured → recognition → FHIR R4 → enrichment → unstructured)
- loinc-unification-direction — Python normalization/LOINC-сервисы → Inngest functions (job-queue-tier → workflow engine)
- llm-proxy-choice — аналог: model-routing/fallback вынесен на инфраструктурный слой (Bifrost), как оркестрация — на Inngest
- gemini-doom-loop — legacy .NET уже делал timeout+restart для recognition-зацикливаний (job-queue-tier механизм)
- health-report-vocabulary — словарь стадий пайплайна несогласован (смежная боль)
Источники
Сноски
-
[Temporal: The definitive guide to Durable Execution](, accessed 2026-05-17, https://temporal.io/blog/what-is-durable-execution — · SE Radio 596: Maxim Fateev on Durable Execution · Temporal Server Architecture. ↩
-
[How Inngest Functions Are Executed](, accessed 2026-05-17, https://www.inngest.com/docs/learn/how-functions-are-executed — · Inngest Steps · Inngest Connect (WebSocket) · Inngest Self-Hosting · Principles of Durable Execution (Inngest). ↩
-
[Vercel Workflow DevKit Blog](, accessed 2026-05-17, https://vercel.com/blog/introducing-workflow — · Vercel WDK Docs · Vercel WDK GitHub. ↩
-
[Hangfire Documentation](, accessed 2026-05-17, https://docs.hangfire.io/ — · Hangfire Batches. ↩
-
[The Emerging Landscape of Durable Computing (Golem)](, accessed 2026-05-17, https://www.golem.cloud/post/the-emerging-landscape-of-durable-computing — · The Rise of the Durable Execution Engine. ↩