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 шага
- До model request — безопасно повторить, но учитывать cost/rate limit.
- После model output до сохранения decision — повтор может дать другое решение.
- После
tool_requestedдо effect — resume видит intent. - После effect до
tool_completed— outcome неопределён; нужен operation ID/reconciliation. - После checkpoint до следующего model call — resume продолжает без повтора effect.
- Во время 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
| Failure | Retry? | Дополнительное условие |
|---|---|---|
| Connection failed до send | Обычно | Bounded budget |
| Timeout после send | Только с idempotency/reconcile | Outcome неизвестен |
| 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 decision | interrupted или fence устарел | не начинать tool; вернуть актуальный terminal status |
| Policy требует approval | interrupt появился во время policy check | записать call-scoped pause под interrupt overlay; tool не запускать |
| Policy разрешила | run уже interrupted | не начинать effect |
| Tool успешно завершился | interrupt появился во время effect | один раз записать фактический outcome, сохранить interrupt сверху |
| Tool дал retryable error | run 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 | Минимум кода | Потеря при crash | Money/long-running |
| DB checkpoint + worker | Контроль | Timers/concurrency самим | Сложные долгие workflows |
| Event log + reducer | Audit/replay | Event evolution | One-shot chat |
| Durable workflow engine | Timers, retries, queues | Operational/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?