Перейти к основному содержимому

Durable execution

После главы вы сможете перечислить crash windows agent step, спроектировать checkpoint protocol и не путать retry transport call с повтором business effect.

Процесс обязательно упадёт

Наивный loop живёт в памяти одного HTTP request. Он теряет состояние при deploy, timeout, worker eviction или restart. Long-running system должна считать process временным, а run — durable.

Failure boundaries шага

  1. До model request — безопасно повторить, но учитывать cost/rate limit.
  2. После model output до сохранения decision — повтор может дать другое решение.
  3. После tool_requested до effect — resume видит intent.
  4. После effect до tool_completed — outcome неопределён; нужен operation ID/reconciliation.
  5. После checkpoint до следующего model call — resume продолжает без повтора effect.
  6. Во время final delivery — пользователь мог не получить ответ, хотя run completed.

«Exactly once» обычно раскладывается на durable intent + at-least-once delivery + idempotent/transactional domain operation.

Lease и fence — разные гарантии

Lease отвечает «кто сейчас имеет право работать», TTL — «когда можно попытаться забрать run», renewal удерживает это право во время долгого await. Fencing token отвечает на более жёсткий вопрос: «не стал ли этот worker устаревшим уже после takeover». Store принимает worker events только с текущим token и ожидаемым sequence; control-plane interrupt записывается отдельным путём.

Это защищает event log, но не умеет отозвать HTTP-запрос, уже ушедший провайдеру. Для write передавайте стабильный operation ID и, где возможно, fence downstream. Поэтому внешний effect остаётся at-least-once boundary, а не магическим exactly-once вызовом.

Durable step protocol

1. validate decision
2. append intent/pending event
3. commit checkpoint
4. execute with stable operation ID
5. obtain/reconcile outcome
6. append completion event
7. project next state

Checkpoint workflow engine не делает внешний API идемпотентным. Domain adapter обязан распознавать повтор.

Replay determinism

Event reducer должен быть pure и deterministic. Model call, clock, random, network и tool execution не запускаются внутри reducer; их результаты уже записаны events.

Если schema события меняется:

  • поддерживайте old versions;
  • используйте upcaster/migration;
  • не переписывайте audit history без отдельной политики;
  • проверяйте replay на historical fixtures.

Учебный InMemoryStateStore сохраняет input+events и каждый раз строит projection reducer’ом.

Retry taxonomy

FailureRetry?Дополнительное условие
Connection failed до sendОбычноBounded budget
Timeout после sendТолько с idempotency/reconcileOutcome неизвестен
429/temporary unavailableДаBackoff + jitter + deadline
Schema invalidНет автоматическиНужно новое model decision
Policy deniedНетИзменить authority нельзя retry’ем
Permanent domain errorНетИзменить input/plan
Approval requiredЭто pauseЖдать human event

Backoff metadata должно быть durable: attempt, next eligible time, total deadline, last failure. Tests не обязаны реально ждать; injected clock делает переходы детерминированными.

Timeouts

Разделяйте:

  • model deadline;
  • tool connect/read deadline;
  • workflow/run deadline;
  • business SLA;
  • approval expiry.

Короткий network timeout не означает, что run failed. Он может перейти в reconciling, получить status по operation ID и только затем retry/compensate.

Pause, interrupt, resume

Approval — persisted state, не await promise в памяти процесса. Checkpoint содержит exact pending tool и arguments. Решение человека — новый event:

  • approve — выполнить сохранённое;
  • edit — заменить arguments и снова валидировать/policy-check;
  • reject — записать typed observation и продолжить/завершить.

Interrupt также event. Resume возвращает run к предыдущему status; если он ждал approval, model call не происходит до решения.

Approval привязан к callId, а не является глобальным boolean внутри run. Retry attempt также выводится из событий текущего pending call. Новый tool cycle — даже если модель повторно использовала текстовый ID после завершения прошлого цикла — не наследует чужой approval или исчерпанный retry budget.

Что делать после каждого await

Асинхронный результат нельзя применять к локальному snapshot автоматически: пока worker ждал, control plane мог записать interrupt, а lease — перейти другому worker.

Вернувшийся результатAuthoritative state после повторного loadДействие
Model decisionвсё ещё running, lease/fence нашиappend decision
Model decisioninterrupted или fence устарелне начинать tool; вернуть актуальный terminal status
Policy требует approvalinterrupt появился во время policy checkзаписать call-scoped pause под interrupt overlay; tool не запускать
Policy разрешилаrun уже interruptedне начинать effect
Tool успешно завершилсяinterrupt появился во время effectодин раз записать фактический outcome, сохранить interrupt сверху
Tool дал retryable errorrun interruptedзаписать attempt/retry schedule, но не делать следующий attempt до resume

Final result строится из reducer-derived terminalCause, а не из последней локальной ветки worker. Так поздний результат не превращает interrupted run в выдуманный completed.

Compensation и saga

Не всякую операцию можно откатить транзакцией. Saga описывает локальные commits и компенсирующие действия:

reserve funds → reserve cargo slot → book carrier
failure carrier → release slot → release funds

Compensation:

  • тоже может упасть и должна быть durable/idempotent;
  • не всегда возвращает мир в исходное состояние;
  • требует audit и manual escalation;
  • не должна генерироваться моделью без domain contract.

Варианты

РешениеВыигрышЦенаКогда плохой выбор
In-memory loopМинимум кодаПотеря при crashMoney/long-running
DB checkpoint + workerКонтрольTimers/concurrency самимСложные долгие workflows
Event log + reducerAudit/replayEvent evolutionOne-shot chat
Durable workflow engineTimers, retries, queuesOperational/semantic overheadМалый synchronous endpoint

Helios и реальный аналог

Helios резервирует SOL, затем worker падает. Повтор с op-reserve-1 возвращает прежнюю reservation. Реальные аналоги: payment idempotency, order fulfillment saga, travel booking, telehealth appointment reservation.

Лаборатория

npm run test:course:pattern -- "crash|approval|interrupt|retry|timeout|permanent"

Нарисуйте crash windows для внешней отправки письма, у которой нет idempotency API. Возможные решения: outbox с собственным provider key, pre-send lookup, dedupe downstream, manual reconciliation. У каждого есть остаточный риск.

Self-check

  • Что сохранено до effect?
  • Кто обеспечивает idempotency?
  • Как узнать outcome после timeout?
  • Может ли reducer вызвать model/tool?
  • Как истекает approval?
  • Какие compensation steps сами требуют retry?
  • Что произойдёт при двух concurrent resume?