ADR-036 — Инциденты: техническая ошибка как долговечное, операбельное состояние¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.1 |
| Дата | 2026-08-04 |
| Владелец | Руслан Габитов |
| Уточняет | SAD-001 v.1.1 §10 (персистентность как состояние-запись — этот ADR делает частью этой записи и сбой), ADR-021 v.1 §2.8 (полноценная конструкция Incident, отложенная до этого ADR), ADR-022 v.2 (политика распространения ошибок, чьё следствие «вершина горутины» здесь уточняется) |
| Связано | ADR-001 v.6 (дорожки и проекция токенов), ADR-007 v.2.1 (модель ожиданий, к которой присоединяется инцидент), ADR-013 v.2 (фаза Incident, зарезервированная в таксономии фактов), ADR-017 v.1 (цикл как единственный владелец состояния), ADR-025 v.2 (взаимодействие с multi-instance), ADR-026 v.1 (взаимодействие с журналом завершений), ADR-033 v.3 (чекпоинт, на котором инцидент едет) |
1. Контекст и проблема¶
Нынешний контракт движка для технической ошибки — фатальный и невидимый. Когда активность падает с чем-либо, кроме смоделированной BPMN-ошибки, падающая дорожка завершается, её токен потребляется, все параллельные дорожки останавливаются, и экземпляр падает целиком. Персистентность не фиксирует ничего: упавшая дорожка — не чекпоинтируемое состояние, а долговечная запись упавшего экземпляра говорит лишь, что он завершился. Единственный механизм повторов у движка живёт ниже цикла экземпляра — политика повторов диспетчера заданий для внешних воркеров (ADR-021 v.1 §2.7), — и когда она исчерпана, терминальный сбой всплывает ровно тем же фатальным и невидимым образом.
Для встраиваемой библиотеки это была оправданная ранняя позиция: хост-процесс наблюдает поток фактов и сам владеет супервизией. Для продакшен-движка — нет. Преходящий инфраструктурный сбой — секундный провал базы внутри одной сервисной задачи — не должен уничтожать экземпляр, несущий часы корректной работы в других ветвях, а сбой, не оставляющий долговечной записи, никто не может увидеть, диагностировать, повторить или разрешить. ADR-021 v.1 §2.8 назвал этот разрыв явно и отложил «полноценную конструкцию Incident (реестр застрявших заданий + операторская поверхность разрешения, экземпляр остаётся жив)» до собственного ADR. Это тот самый ADR.
Стандарт здесь молчит — намеренно. BPMN моделирует бизнес-сбой —
событие Error, его перехват на границе, разрешение по цепочке областей — и
ничего не предписывает о технической ошибке. Вендоренный экстракт говорит
прямо: для неразрешённого триггера Error «спецификация не предписывает
конкретной реакции движка» (semantics/event-handling.md, unresolved
trigger); экстракт в целом не содержит понятий инцидента, повтора или
технической ошибки. Всё, что решает этот ADR, — выбор движка,
регистрируемый как таковой, с моделью инцидентов Camunda как заявленной
целью выравнивания проекта.
2. Решение¶
2.1 Инцидент¶
Инцидент — долговечная запись сбоя, который модель не обработала: он называет экземпляр, узел, родословную упавшей дорожки, цепочку причин, счётчик попыток, времена первого и последнего сбоя — и снимок данных на момент сбоя: переменные, видимые в цепочке областей падающего узла, снятые при каждом поднятии, чтобы оператор видел то, что видела попытка, а не то, чем область стала позже. Это состояние, а не факт: он живёт в состоянии-записи экземпляра, переживает рестарты через чекпоинт и существует, пока не разрешён. Поток наблюдаемости его анонсирует (§2.7), но никогда не несёт — поток фактов остаётся best-effort и с потерями по построению (ADR-013 v.2).
Что его поднимает:
| Сбой | Реакция |
|---|---|
| Техническая ошибка активности — ошибка внутрипроцессной задачи, сбой потока данных, ошибка выражения | Инцидент на падающем узле |
| Исчерпаны повторы задания — диспетчер ADR-021 сдался на задании внешнего воркера | Инцидент на сервисной задаче; диагностика задания (топик, попытки, последняя ошибка) становится причиной инцидента |
| Неперехваченная BPMN-ошибка, брошенная падающей активностью — нет границы, нет перехватчика в цепочке областей | Инцидент на бросившем узле. Стандарт оставляет реакцию открытой (§3); восстановимая остановка доминирует над смертью, потому что неперехваченная ошибка активности — как правило ошибка развёртывания, отсутствующая граница, — исправимая правкой модели и повтором на живом состоянии |
| Error End Event, дошедший до корня неперехваченным | Экземпляр падает, как сегодня. Это смоделированный исход — автор явно завершил экземпляр ошибкой (semantics/end-events.md: Error End Event роняет экземпляр; Camunda так же завершает область, без инцидента). Превращать вердикт самой модели в операторский тикет значило бы инвертировать её замысел |
| Нарушение инварианта — невозможное состояние движка | Падение экземпляра, как сегодня. Инцидент утверждает: «внешний мир повёл себя плохо, состояние движка цело»; нарушение инварианта отрицает вторую половину, и повтор на испорченном состоянии лишь усугубляет |
| Любой неперехваченный сбой в дочернем экземпляре (вызываемый Call Activity, на любой глубине) | Дочерний экземпляр падает, и сбой распространяется через границу вызова к узлу Call Activity вызывающего; типизированные ошибки остаются перехватываемыми его границей. Инциденты существуют только у экземпляров верхнего уровня: для вызывающего весь вызванный процесс — одна задача с двумя исходами, успех или сбой, и инцидент, возникающий в итоге на call-узле (верхнего уровня), несёт диагностику потомка и предлагает честную единицу повтора: перезапустить весь Call Activity — либо resolve/отказаться |
Каждый инцидент при поднятии логируется на уровне Error — неразрешённый
сбой никогда не молчит (действующее правило uncaught-must-log), и дисциплина
ADR-022 v.2 «один сбой — одна запись» сохраняется: инцидент и есть точка
обработки, так что запись в лог сопровождает инцидент, и выше ничто не
логирует повторно.
2.2 Завершённая дорожка, открытый инцидент — сохранение токена¶
Падающая дорожка завершается — в новом терминальном состоянии
TrackIncident, — а запись инцидента берёт на себя роль долговечного
носителя всего, что нужно будущей попытке: узел, путь области, родословную,
цепочку причин, счётчик попыток. Это паттерн дегидратации, применённый к
сбою (ADR-007 v.2.1): носитель горутины заканчивается, записанное состояние —
то, что остаётся, а продолжение — свежая дорожка, порождённая из записи,
ровно так же, как перехват на границе рождает новую дорожку на границе, а
регидратация — из чекпоинта. Каждый повтор — собственная дорожка с упавшей в
качестве предшественника, так что история попыток — обычная родословная
дорожек, а не бухгалтерия, выдуманная под инциденты.
Узел был входен, но не завершён: ни один исходящий поток не срабатывает, завершение не записывается, взведённые граничные события остаются взведёнными (§2.4), и инцидент удерживает свою область открытой — повтор входит заново на тех же данных, так что область не может быть закрыта, пока жив инцидент. Пока инцидент открыт, токен остаётся видимым на падающем узле — проекция выводит его из записи открытого инцидента, а не из терминальной дорожки: сохранённый токен, который не видно ни в одном представлении, не сохранён ни в каком смысле, важном для оператора.
Следствия, сформулированные как контракт:
- Параллельные ветви не затронуты. Они продолжают исполняться; инциденты могут накапливаться. Один застрявший платёжный вызов больше не уничтожает ветвь отгрузки.
- Экземпляр остаётся жив. Его состояние — активен с инцидентами: запрашиваемый предикат, а не терминальное состояние. Экземпляр, чьи единственные оставшиеся продолжения — инциденты, ждущие оператора, не держит ни одной горутины и покоен, как любой полностью ждущий экземпляр (ADR-007 v.2.1); экземпляр с запланированным повтором остаётся резидентным до дедлайна — его дегидратация относится к интеграции держателей ожиданий в более позднем срезе, и в любом случае горутины на каждый повтор не бывает.
- Завершение ждёт. Экземпляр не может завершиться, пока на нём открыт инцидент; расклинивает его разрешение (§2.6).
2.3 Повторы: два слоя, каждый со своим предметом¶
Владение повторами делится по линии, проведённой ADR-021 v.1:
- Повтор задания — первая линия, без изменений. Политика диспетчера (попытки, backoff, джиттер; per-service и общедвижковый дефолты) продолжает повторять задания внешних воркеров ниже цикла, без смены состояния дорожки и без инцидента. Инцидент начинается там, где эта автоматика кончается.
- Повтор инцидента — когда автоматика сдалась. Открытый инцидент можно повторить: из записи инцидента (§2.2) порождается свежая дорожка на упавшем узле с упавшей дорожкой как предшественником — свежее исполнение активности на текущих данных области, под тем же контрактом, что и повторный вход при восстановлении из чекпоинта (ADR-033 v.3: шаг атомарен, восстановление входит в узел, никогда — в полушаг; эффекты остаются at-least-once).
Скрытого автоматического цикла повторов на уровне дорожки нет. Сбой внутрипроцессной задачи идёт прямо в инцидент. Смягчает это политика повторов инцидента — попытки и backoff, настраиваемые на активность и общедвижковым дефолтом, — вычисляемая вне цикла, по паттерну ADR-021 v.1 §2.4: запланированный повтор — персистентное состояние плюс дедлайн, структурно таймерное ожидание, без спящей горутины. Политика с нулём попыток означает, что каждый инцидент ждёт оператора; дефолт движка намеренно консервативен (повтор помогает преходящим сбоям, а детерминированный баг, повторённый N раз, — это N одинаковых сбоев: счётчик попыток и неизменная причина делают это видимым, а не шумным).
2.4 Взаимодействия — три контрактных вопроса¶
Граничные события. Инцидент не завершает активность, поэтому взведённые границы остаются взведёнными. SLA-таймер продолжает тикать над застрявшей активностью — ровно тот случай, когда SLA важнее всего, — и непрерывающий таймер может сработать, пока инцидент открыт. Прерывающая граница, сработавшая на узле с инцидентом, отменяет узел и закрывает инцидент как overtaken: модель приняла решение, которое принял бы оператор. Повтор входит в узел без перевзвода его границ — многократные падения не должны сбрасывать SLA-часы (жизненный цикл границы начался при первом входе в узел, и по жизненному циклу активности (§13.3.2) узел не покидал активной фазы).
Компенсация. Журнал завершений (ADR-026 v.1) записывает только завершённую работу. Узел с инцидентом в журнал не входил; активность «упала-потом-повторилась» входит в него ровно один раз — на том завершении, которое в итоге удалось. Упавшие попытки — история инцидента, не журнала.
Multi-instance. Сбой одного внутреннего экземпляра поднимает инцидент только на этом внутреннем экземпляре — каждый внутренний экземпляр изолирован в собственной per-instance области (ADR-025 v.2 §2.2), и эта область идентифицирует инцидент. Соседние экземпляры доходят до собственного завершения; MI-узел не может завершиться, пока инцидент открыт, — правило §2.2 уровнем ниже. Повтор перезапускает только упавший экземпляр: порождённая дорожка входит во внутреннюю активность в той же per-instance области и видит ровно ту идентичность итерации и тот срез данных, что видела упавшая попытка, а учёт завершения узла — уже ждущий открытый слот этого экземпляра — не затронут. Завершённые внутренние экземпляры никогда не перезапускаются: это завершённая работа в журнале, их эффекты существуют; модель, желающая семантики «всё или ничего» по набору, говорит это граничной ошибкой плюс компенсацией — смоделированным, бизнес-уровневым путём. Одна асимметрия стоит того, чтобы её назвать: в последовательном MI инцидент экземпляра k по определению блокирует k+1…N, так что остаток последовательности ждёт за повтором. Повтора всего набора и падения всего набора не существует.
2.5 Персистентность — инцидент едет на чекпоинте¶
Инциденты аддитивно расширяют документ ADR-033 v.3:
- Поднятие инцидента — точка персиста. Переход в
TrackIncidentгейтирует чекпоинт, как переходы ожиданий: инцидент, исчезающий вместе с процессом, не был бы инцидентом вовсе. - Запись инцидента — элемент уровня экземпляра в документе — терминальные дорожки, как и сегодня, не персистятся; персистится сам инцидент, несущий всё нужное респауну (узел, путь области, родословную, цепочку причин, счётчик попыток, дедлайн повтора при запланированной политике, времена первого/последнего сбоя). Структурно это собрат дескриптора ожидания: восстановление перевзводит запланированный повтор, перевыводя его дедлайн, а ждущий оператора инцидент просто персистится.
- Словарь персистентного статуса экземпляра получает состояние активен с инцидентами, так что первый вопрос оператора — «что требует меня?» — отвечается из хранилища без загрузки экземпляров.
- Оговорка о хранении снимка. Снимок на момент сбоя (§2.1) делает инцидент самодостаточным для диагностики — по цене, названной открыто: данные, попавшие в инцидент, включая dead-lettered, переживают обычный жизненный цикл данных экземпляра. Выбор и исключение значений (чувствительные данные, размер) — механизм маркеров данных, отложенный в §5; пока его нет, снимок — полная видимая область.
- Запись мёртвых писем. Отказ от инцидента (§2.6) сам по себе долговечен: инцидент закрывается как dead-lettered, его запись сохраняется со всей историей. Слив, переигрывание и удаление мёртвых писем — операторский процесс, принадлежащий серверу, не библиотеке; обязанность библиотеки — чтобы запись существовала и выживала.
2.6 Разрешение — поверхность библиотеки¶
Движок владеет примитивами; любой операторский UX их компонует:
| Операция | Смысл |
|---|---|
| Запрос | Открытые инциденты по экземпляру, узлу, классу причины, возрасту; предикат активен с инцидентами на уровне хранилища |
| Повторить сейчас | Немедленно породить входящую заново дорожку (§2.3), невзирая на остаток бюджета политики |
| Resolve | Закрыть инцидент как обработанный вне движка: свежая дорожка порождается с исходящих потоков узла — как если бы узел завершился с текущими данными области, без его перезапуска; оператор утверждает, что эффект работы существует |
| Отказаться | Закрыть как dead-lettered (§2.5); дорожка отменена, и экземпляр далее может быть завершён или скомпенсирован следующим действием оператора |
Skip / перенос токена намеренно отсутствует — перемещение токена есть примитив вмешательства с собственной семантикой (куда ему можно приземлиться, что перевзводится), отдельное будущее решение; смешать его с разрешением инцидентов значило бы протащить тот контракт сюда. Пока его нет, resolve покрывает честное большинство: оператор починил мир, и процесс может идти.
2.7 Наблюдаемость¶
Зарезервированная фаза Incident в таксономии ADR-013 v.2 активируется:
raised, retry-scheduled, retried, resolved, dead-lettered, overtaken — каждый
факт на потоке несёт идентичность инцидента и класс причины. Поток остаётся
анонсом, не записью (§2.1): потерянный факт теряет уведомление — никогда
инцидент.
3. Обоснование¶
| Утверждение | Источник |
|---|---|
| Спецификация не предписывает реакции на неразрешённый Error; инцидентов/повторов в стандарте нет | вендоренный экстракт, semantics/event-handling.md («unresolved trigger»: «спецификация не предписывает конкретной реакции движка»); ни одной клаузы об инцидентах/повторах/технической ошибке во всём экстракте |
Прерывающий Error приостанавливает исполнение в точке броска; жизненный цикл активности имеет состояние Failing, отличное от завершения |
state-machines/activity-lifecycle.md (BPMN §13.3.2); semantics/event-handling.md («Errors are critical») |
| Перехват границей / цепочкой областей — смоделированный путь, он не тронут | semantics/event-handling.md (первое совпадение потребляет триггер) |
Конструкция Incident была явно отложена до этого ADR; «инцидентоподобным» до сих пор был Failed + диагностика |
ADR-021 v.1 §2.8, §7 |
| Повтор заданий ниже цикла: re-enqueue с backoff, без спящей горутины, до цикла доходит только терминальный исход | ADR-021 v.1 §2.4, §2.7 |
| Классификация бизнес-ошибка vs технический сбой — вне цикла | ADR-021 v.1 §2.6 |
| Один сбой ⇒ одна точка обработки ⇒ не более одной записи в лог; вершины горутин — граница сбоев | ADR-022 v.2 §2.1, §2.3 |
| Переходы чекпоинтов гейтируют персист; шаг атомарен — восстановление входит в узел; эффекты at-least-once, состояние exactly-once | ADR-033 v.3 §2.1–§2.3 |
| Поток фактов — best-effort, с потерями, read-only по построению; никогда не несущее состояние | ADR-013 v.2 |
Incident — зарезервированная, неиспользуемая фаза в таксономии фактов |
ADR-013 v.2 (каталог JobState и зарезервированные слоты) |
| Журнал завершений записывает только завершённую работу | ADR-026 v.1 §2.1 (журнал), §2.7 (его жизненный цикл) |
| Выравнивание с Camunda: инцидент при исчерпании повторов и при неперехваченной ошибке, экземпляр жив, оператор повторяет/разрешает | семантика инцидентов Camunda 7 (заявленная цель выравнивания проекта) |
4. Рассмотренные альтернативы¶
| Альтернатива | За | Против | Решение |
|---|---|---|---|
| A. Статус-кво — фатальные для экземпляра сбои, супервизия снаружи (хост смотрит факты, перезапускает работу) | никаких изменений движка; максимальная простота | сбой не долговечен, внешней супервизии нечего читать после краха; один преходящий сбой уничтожает непричастную корректную работу; непригодно как позиция замены Camunda | ❌ отклонено |
| B. Автоматический повтор на уровне дорожки внутри движка — дорожка перезапускает упавший узел N раз, прежде чем что-то всплывёт | прозрачно для моделей; нет нового состояния | скрытые повторы маскируют детерминированные баги под медлительность; переисполнение без долговечной записи невидимо нарушает дисциплину at-least-once; противоречит правилу «без спящей горутины» либо блокирует цикл | ❌ отклонено — повтор становится видимым состоянием инцидента (§2.3) |
| C. Инциденты только как факты — богатые факты сбоя, состояние остаётся терминальным | крошечное изменение; таксономия уже зарезервирована | поток по построению с потерями; «запись», которую можно уронить, — не запись; ничего не переживает рестарт; нет цели для повтора | ❌ отклонено — инцидент есть состояние (§2.1) |
| D. Полный паритет с Camunda — инциденты и для бизнес-ошибок, промахов вычисления условий, промахов корреляции сообщений | одна единая поверхность сбоев | стирает линию смоделированное/техническое из ADR-021 §2.6: перехваченная бизнес-ошибка — нормальный исход, а превращение семантики модели в операторские тикеты инвертирует замысел самого BPMN | ❌ отклонено — смоделированные пути остаются смоделированными; инцидентом становится только неперехваченный конец |
| E. Инцидент как живое, припаркованное состояние дорожки — падающая дорожка выживает, остановленная на узле как ожидание, повтор её возобновляет | нет новой записи уровня экземпляра; проекция токенов не меняется | изобретает третий класс живости рядом с ожиданием/дегидратацией — «живую» дорожку без горутины и без дескриптора ожидания, — которую обязана особо учитывать вся бухгалтерия живых дорожек; у возобновления упавшего исполнения нет готовой машинерии, у респауна их две (порождение на границе, регидратация) | ❌ отклонено — паттерн дегидратации уже решает эту форму |
| F. Инцидент как терминальная дорожка + долговечная запись уровня экземпляра, продолжение порождением свежей дорожки из записи (повтор: на узле; resolve: с его исходящих потоков), двухслойные повторы, примитивы разрешения у библиотеки (выбрано) | переиспользует машинерию порождения/регидратации; история попыток — обычная родословная дорожек; сохраняет работу и токен (проецируемый из открытого инцидента); запрашиваемо из хранилища | новое терминальное состояние, расширение чекпоинта и изменение словаря статусов; проекция токенов должна выучить один новый источник | ✅ выбрано |
5. Отложенное¶
- Перенос токена («skip» / административные перемещения токена) — собственное решение о примитивах вмешательства; §2.6 фиксирует границу. Для того будущего ADR уже записаны две вещи: механизм респауна §2.2 — его предполагаемый субстрат («поставить токен на узел X» — тот же примитив, что повтор инцидента: породить дорожку на X с записанной родословной), а ограничение областью — токен можно перемещать только внутри его текущей области — стартовая гипотеза правила легальности, поскольку она обходит перенос в область, чья инициализация не выполнялась. Что там по-настоящему открыто: ожидания join'ов для унесённого токена, что перевзводится в цели, и раздел авторизации/аудита с сервером.
- UX слива/переигрывания мёртвых писем — операторская поверхность сервера; библиотека записывает (§2.5), сервер оперирует.
- Аналитика истории инцидентов (среднее время разрешения, кластеризация сбоев) — забота хранилища истории/аудита, когда оно появится.
- Маркеры/теги данных — общий механизм аннотаций на объявленных данных, из которого посмертный отбор (§2.5) — лишь один случай: хранение в снимках инцидентов, видимость в операторских поверхностях, редактируемость оператором, исключение чувствительных данных/PII и любые дальнейшие расширения, цепляющиеся к объявленным данным, а не к коду. Решение уровня моделирующей поверхности для собственного ADR; отслеживается в трекере проекта.
- Семантика массового разрешения — композиция операторской поверхности; примитивы библиотеки — per-incident.
История документа¶
| Версия | Дата | Автор | Изменение |
|---|---|---|---|
| v.1 | 2026-08-04 | Руслан Габитов | Первоначальное решение: инцидент как терминальная дорожка + долговечная запись уровня экземпляра, продолжение респауном (паттерн дегидратации); двухслойные повторы; контракты границ/компенсации/MI; расширение чекпоинта; примитивы разрешения |