Skip to content

ADR-013 — Наблюдаемость и управление (один event-seam, на весь движок)

Поле Значение
Статус Accepted
Версия v.2
Дата 2026-07-11
Владелец Руслан Габитов
Уточняет ADR-002 v.2 Extension Architecture
Смежные ADR-022 v.1 Error Propagation and Logging Policy — log-канал, в который эхом пишет единый producer этого ADR

EN-оригинал — канонический: ADR-013-instance-observability.md. Этот файл — его перевод (twin). При расхождении приоритет у английского текста.

Концепция, теперь приземлённая сопровождающим wiring-SRD; v.2 замещает v.1 как принятый контракт (эмиссия DataChange — единственное ⏳ отложение: её словарь приземлён, а её проводка едет с переработкой data-plane ADR-011). v.1 исправил находку аудита 2.2 (публичный API write-only) механизмом наблюдения-и-управления: публичный InstanceHandle, один lifecycle-канал, в который публикуют узлы и задачи, явное грубое управление, жизненный цикл движка (Shutdown, UnregisterProcess) и стандартно-названный открытый словарь состояний. v.2 завершает покрытие: приземлённая форма v.1 была намеренно минимальной (видеть, что происходит внутри одного Instance); v.2 определяет полную таксономию наблюдаемых событий через все главные объекты движка — движок, event hub, регистрация процессов, instance, узлы, решения шлюзов, события, корреляция, worker-jobs, user-tasks, boundary, faults, данные — эмитируемую через один producer, который и питает observer-поток, и пишет эхо в operator-log (уровни по ADR-022 v.1), и добавляет engine-scope observer registry, чтобы не-instance-события тоже были наблюдаемыми: один согласованный вид gobpm. Seam политики видимости (опциональные capabilities на расширении авторизации; pass-through, когда не реализовано) позволяет embedder'у скрывать или редактировать события per-recipient — сама модель политики едет с будущей IAM-работой. Мутирующие per-node-listener'ы остаются отвергнутыми (скрытое управление, ADR-011); грубое операторское управление — не оно. Концепция прескриптивна; сопровождающий SRD code-grounded.

1. Контекст

1.1 Что встраиваемый движок должен своему host'у

gobpm встраивается в приложение (SAD-001). Сегодня, как только процесс стартует, host слеп — у него нет ничего, кроме строк лога и вывода в консоль, чтобы догадываться о прогрессе. Встраиваемый движок должен вместо этого позволять host'у:

  • Observe — состояние жизненного цикла instance, где его токены и как они движутся, какие узлы исполняются и их прогресс, его данные и его исход (блокироваться до завершения).
  • Control — грубые, явные операторские действия над работающим instance: отменить его; (позже) приостановить и возобновить его.
  • React — цепляться за определённые моменты жизненного цикла (события instance/узла/flow/задачи), чтобы управлять UI, аудитом или интеграцией.

Это не BPMN-нормативно (API встраивания определяется движком), так что это продуктовое/архитектурное решение, обоснованное встраиваемостью.

1.2 Что у движка есть сегодня

Механизм v.1 приземлён: InstanceHandle (состояние, токены, ридер данных, WaitCompletion, Cancel), per-instance observer-поток с его асинхронным lossy контрактом доставки и жизненный цикл движка (Shutdown, UnregisterProcess) — всё существует. Находка write-only-API (audit 2.2) закрыта.

Но приземлённое покрытие намеренно минимально — достаточно, чтобы видеть, что происходит внутри одного Instance, и не более. Аудит полноты обоих каналов (observer-поток и operator-log, после ремедиации ADR-022 v.1) показывает:

  • Observer-таксономия несёт два вида событий: состояние жизненного цикла instance (и даже не Created) и событие node-progress, свёрнутое в трёхзначную проекцию токена — listener не может отличить узел, входящий, от исполняющегося, от покидаемого, ни завершённый узел от упавшего, отменённого или слитого.
  • Всё вне Instance невидимо observer'ам: жизненный цикл движка и event-hub, регистрация/снятие регистрации/суперседенс версий процесса, весь жизненный цикл external-worker-job, решения корреляции, взаимодействия user-task, арминг и firing boundary-событий — всё это богато логируется с ADR-022, но ничто не наблюдаемо. Обратное тоже верно: флипы состояния instance доходят до observer'ов, но не пишут лога, хотя ADR-022 §2.4 называет вехи жизненного цикла заботой уровня Info.
  • Некоторые переходы молчат на обоих каналах: решение о ветке шлюза, взятие user-task, арминг/дизарминг boundary — и, острее всего, boundary-пойманная BPMN-ошибка, которая сегодня не оставляет следа нигде (только непойманный fault всплывает как instance-fault Error).
  • У плоскости данных есть дремлющий механизм change-notification (data.UpdateCallback, с видами изменений added/updated/deleted и асинхронным fan-out'ом), который не потребляет никакой код движка — не подключён ни к одному каналу.

Так что у host'а два полу-вида, которые даже не пересекаются согласованно. v.2 существует, чтобы сделать вид полным и согласованным: каждый сбой и каждый переход жизненного цикла главного объекта наблюдаем, на весь движок, через один producer.

1.3 Почему строить механизм сейчас (и аргумент про seam)

Две причины не ждать:

  • Пользователи слепы сегодня. Видимость прогресса — это базовая ставка; «читай логи» — не ответ для встраиваемой библиотеки.
  • Seam дешевле всего приземлить до узлов, которые его питают. SendTask/ ReceiveTask (ADR-014) и каждый будущий узел должны докладывать прогресс через один канал. Построить этот канал сейчас означает, что новая работа по узлам/задачам включается в него; ретрофит наблюдения в каждый узел позже — дорогой путь.

1.4 Согласование с неустоявшейся моделью состояний

Законное опасение (поднятое на ревью) — что наши состояния жизненного цикла ещё не полны по стандарту: жизненный цикл instance частичен (Created → Active → Completed, Terminating → Terminated; Failing/Failed и Paused отложены в будущие ADR), а activity-жизненный цикл BPMN (§13.3.2: Ready → Active → Completing → Completed, Withdrawn/Compensating/…) не смоделирован вовсе. Выставление замороженного enum состояний публично сейчас churn'ило бы публичный API, когда эти подсистемы приземлятся.

Разрешение разделяет механизм и словарь:

  • Механизм — handle, канал, операции управления — это стабильный публичный контракт, решённый здесь.
  • Словарь — множество состояний жизненного цикла и видов узлов, о которых докладывает механизм, — назван по стандарту BPMN для подмножества, которое у нас уже есть, и оставлен открытым множеством. Именование по стандарту дёшево (это именование, не реализация error/compensation/suspend); а добавление состояний/видов позже аддитивно и неломающе. Так мы получаем видимость сейчас без заморозки неполного enum'а: отложенные состояния (Failing, Paused, Compensating, под-состояния activity) присоединяются к тому же словарю по мере приземления их подсистем.

Это прямой ответ на «выравнивать по стандарту или расширять позже?» — выровнять имена сейчас (стабильно), расширять множество аддитивно.

2. Решение

2.1 Публичный InstanceHandle — наблюдать и управлять

Старт процесса возвращает публичный InstanceHandle (находимый по id) — окно host'а в один instance:

  • State() — состояние жизненного цикла, из стандартно-названного открытого словаря (§2.4), читается без локов.
  • Token view — снимок того, где исполнение (какие узлы держат токены), и поток token-movement-событий через канал (§2.2), так что host может следить за прогрессом, а не только сэмплировать его.
  • Node execution progress — какие узлы активны и их прогресс (entered → executing → left), докладываемый самими узлами (§2.2).
  • Data read — process properties + runtime-переменные, read-only, через публичный read-only-ридер данных (ADR-011 v.5 §2.6; кейс «observe from outside», отложенный из ADR-010/011, приземлён здесь).
  • WaitCompletion(ctx) — блокироваться до завершения instance или ctx; возвращает терминальное состояние + любую ошибку (заменяет done-канал из examples).
  • ControlCancel(ctx) сейчас (двигает Terminating → Terminated); Suspend/Resume зарезервированы (им нужна отложенная подсистема Paused, присоединяются аддитивно). Управление грубое и явное — операторское действие над всем instance, записанное в канал.

Наблюдение конкурентно-безопасно (lock-free-состояние, лок плоскости данных, копированный снимок токенов); управление идёт через собственную машину состояний движка, никогда не через чёрный ход.

2.2 Один lifecycle-канал, в который публикуют узлы и задачи

Host регистрирует observer'ов на одном канале. v.1 набросала семейства событий в прозе (instance / token-movement / node-execution / task-события); v.2 заменяет этот набросок канонической таксономией §2.6, которая и есть авторитетный каталог того, что несёт канал.

Критически, узлы и задачи докладывают свой собственный прогресс в этот один канал — это seam, в который включаются будущие реализации узлов/задач (включая SendTask/ReceiveTask из ADR-014). Добавить вид узла означает эмитить его события здесь, а не изобретать новый путь наблюдения.

Observer'ы read-only над данными и flow: они получают событие (id, имена, стандартно-названное состояние, timestamp'ы — никогда payload'ы, по правилу log-masking) плюс read-only-handle, и не могут менять данные или перенаправлять flow. Это следует принципу no-hidden-control ADR-011. (Явное управление жизненным циклом, §2.1, — отдельная, видимая, инициированная оператором вещь, не побочный эффект listener'а.)

Доставка асинхронна и никогда не блокирует track — канал есть блокирующий примитив, так что движок не должен ждать, пока observer прочитает. Контракт:

  • Поток событий best-effort, lossy, упорядоченный. На observer'а: буферизованный канал (размер N), дренируемый одной выделенной горутиной, которая вызывает observer'а; track эмитит неблокирующей отправкой (select { case ch <- ev: default: dropped++ }). Track никогда не блокируется; память ограничена; порядок сохранён (один канал + одна горутина дренажа); медленный observer теряет события и узнаёт сколько (выставленный счётчик dropped), и не может повлиять на движок или других observer'ов. Паникующий observer recover'ится, не пропагируется. Это чистый stdlib (chan + горутина + sync/atomic).
  • Терминальное завершение — единственный гарантированный, блокируемый сигнал — и только потому, что host его попросил. WaitCompletion(ctx) подкреплён done-каналом, который track закрывает на терминальном состоянии (плюс сохранённый результат), никогда не отправкой: закрытие неблокирующе для движка и освобождает всех ждущих разом, так что «completed/failed/terminated» никогда не теряется, хотя lossy-поток может терять события прогресса.

(Кольцо drop-oldest — хранящее свежайший прогресс — возможное более позднее уточнение за тем же observer-контрактом; drop-newest + счётчик — устойчивый дефолт, и это то, что решает этот ADR.)

2.3 Управление жизненным циклом грубое, явное и опосредованное движком

Операции управления действуют над всем instance через машину состояний движка:

  • Cancel(ctx) — запрос завершения; instance проходит Active → Terminating → Terminated, токены withdraw'ятся, канал докладывает об этом. Доступно сейчас.
  • Suspend(ctx) / Resume(ctx) — пауза/продолжение движения токенов. Зарезервированы: им нужны состояние Paused и подсистема suspend, отложенные ADR-001 §4.2; handle объявляет их, чтобы контракт был стабилен, и они активируются, когда эта подсистема приземлится.

Это отлично от отвергнутого мутирующего per-node-listener'а (§4): управление — видимое операторское действие над instance, не невидимая логика, впрыснутая в исполнение узла.

2.4 Словарь состояний и видов узлов стандартно-назван и открыт

Состояния, о которых докладывает механизм, используют стандартные имена BPMN для подмножества, которое существует (process/instance: Active, Completed, Terminating, Terminated; activity, как он смоделирован: Ready, Active, Completed), и множество явно открытоFailing/Failed, Paused, Compensating и остальные под-состояния activity присоединяются к нему аддитивно по мере приземления их подсистем. Публичный контракт — это формы handle и канала, не закрытый enum; host должен обрабатывать неизвестное состояние/вид изящно (forward-compatible). Это делает поверхность наблюдаемости стабильной сегодня и полной по стандарту со временем без ломающего изменения — согласование §1.4.

2.5 Жизненный цикл движка — graceful shutdown и снятие регистрации процесса

  • Thresher.Shutdown(ctx) — graceful stop: перестать принимать старты, утрясти (или отменить по дедлайну) работающие instance и закрыть событийную машинерию и её waiter'ы. Публичный контракт решён здесь; механика владения горутинами waiter'ов / WaitGroup — audit 2.5, приземляется с ADR-006 — Shutdown её публичный потребитель.
  • UnregisterProcess(id) — удалить определение процесса + его snapshot, чиня утечку snapshots (2.2); отвергает (или документирует политику для) удаление с живыми instance.

2.6 Таксономия наблюдаемых событий (v.2) — виды, фазы, scope, log-эхо

Канонический каталог. Каждый вид (kind) называет класс объекта; его фазы — открытое, стандартно-названное множество (дисциплина §2.4 — расширять аддитивно, потребители терпят неизвестное); scope говорит, какой observer-registry видит его нативно (§2.8 — engine-scope observer'ы видят всё); log-эхо — уровень operator-log, который единый producer (§2.7) пишет по семантике ADR-022 v.1 §2.4.

Kind Объект Фазы (открытое множество; ⏳ = зарезервированный слот) Scope Log-эхо
EngineState Thresher Starting, Started, Paused, Stopping, Stopped (⏳Resumed как отдельная фаза — пока возобновление пере-эмитит Started) engine Info
HubState EventHub Started, Stopped, ⏳Paused/Resumed engine Info
ProcessLifecycle определение процесса Registered, Unregistered, VersionSuperseded engine Info
InstanceState instance Created, Active, Terminating, Completed, Terminated, Failed, ⏳Suspended/Resumed instance Info (Failed → Error)
NodeProgress узел на track'е Entered, Executing, Completed, Failed, Canceled, Merged, Parked (выровнено по BPMN §13.3.2, не свёрнуто — §2.10) instance Debug
GatewayDecision шлюз BranchesChosen (выбранные flow(s) в details) instance Debug
EventFlow определение события Registered, Fired, Delivered, Dropped, Unregistered engine Debug
Correlation conversation KeyAssociated, Matched, Mismatched instance Debug
JobState worker-job Enqueued, Locked, Completed, TechnicalFault, BusinessError, RetryScheduled, RetriesExhausted, LockReclaimed, ⏳Incident engine Debug (RetriesExhausted, LockReclaimed → Warn)
TaskState user-task Announced, Taken, Completed, Withdrawn both Info
Boundary boundary-событие Armed, Fired, Disarmed instance Debug
Fault BPMN error / fault Thrown, Caught, Uncaught both Debug (Thrown, Caught — спроектированный путь есть ожидаемое поведение) / Error (Uncaught — fault instance)
DataChange элемент данных Value_Added, Value_Updated, Value_Deleted (существующий словарь видов изменений) instance none — только observer-поток (§2.10, защита по объёму)

Таблица И ЕСТЬ listener-контракт: реализация listener'а подписывается на виды и свитчится по фазам; обе оси растут аддитивно. Правило полноты эмиссии: каждый сбой и каждый переход, названный здесь, ДОЛЖЕН быть эмитирован — не-эмитированный переход каталога есть дефект того же класса, что и молча отброшенная ошибка (ADR-022 §2.6: случайное молчание хуже шума).

2.7 Один Reporter, два канала

v.1 эмитил observer-события и (с ADR-022) operator-логи независимо — что и есть ровно то, как разошлись два полу-вида §1.2. v.2 унифицирует Reporter, а не каналы:

  • Запись — это Fact (§2.7a именует словарь). Каждый переход каталога эмитируется одним вызовомReport(fact) на Reporter рантайма — который внутри (a) пишет эхо в operator-log на уровне §2.6 этого вида, с каноническими ключами атрибутов ADR-022 §2.5, и (b) передаёт Fact в observer-registry его scope (§2.8) под асинхронным lossy контрактом доставки v.1.
  • Одна точка вызова на переход означает, что лог и observer-поток больше никогда не смогут разойтись: полнота обеспечивается на едином seam'е, и ревьюер проверяет одну эмиссию на строку каталога, а не две.
  • Вниз по потоку каналы остаются раздельными ровно как требует ADR-022 §2.7 — лог синхронный и надёжный для операторов, observer-поток best-effort, lossy и неблокирующий для программных listener'ов. v.2 уточняет это правило до: раздельные каналы, единый Reporter.

2.7a Политика репортинга — Fact против диагностики, один ненулевой Reporter

Терминология намеренна: BPMN «Event» — несущий доменный словарь (Start/End/ Boundary-события, Message/Timer/Signal/Error-триггеры), так что запись наблюдаемости — это Fact, никогда не «event». Канонические имена:

  • Fact — единственная запись наблюдения (identity + Kind + Phase + Details; маскировано, никогда не payload), от эмиттера до доставки. Никакой второй «публичной event-проекции» нет.
  • Reporter — joiner за Report(Fact): он эхом отдаёт Fact в operator-log И раздаёт его зарегистрированным observer'ам. Дефолт — только эхо; более богатый Reporter движка добавляет observer-registry.
  • Observer / OnFact(Fact) — ЕДИНСТВЕННЫЙ интерфейс, который реализует host, чтобы наблюдать за движком; host регистрирует его и никогда не строит Reporter. Observer, Fact и словарь Kind/Phase каноничны в pkg/observability.

Граничное правило — репорт есть Fact тогда и только тогда, когда он называет (Kind, Phase) из каталога §2.6 (переход жизненного цикла или сбой первоклассного объекта движка/домена). Всё остальное — диагностика. Это чек-лист, а не вопрос суждения:

  1. Fact каталога эмитируется через единственный Reporter, никогда не Logger()-ится напрямую. Reporter отдаёт его эхом (уровень по таблице §2.6) и раздаёт.
  2. Эмиттер никогда не выбирает уровень эха — это делает таблица kind+phase (неклассифицированный вид всплывает громко). Диагностика логируется на своём месте; быть залогированной есть её цель.
  3. Диагностика использует только Logger() и никогда не фабрикует Kind. Она свободной формы, может нести богатые ошибки/стеки и нацелена на человека, отлаживающего движок, — не на listener мониторинга процессов (расчёт retry backoff, отказ продления подписки, стартовый баннер, ошибка infra-loop).
  4. Уклон в Facts. Если потребитель правдоподобно подписался бы на это — это Fact; растить каталог §2.6, а не оставлять это диагностикой.
  5. Диагностика — небольшое перечислимое множество на модуль; если это множество растёт, перепроверить, не следует ли элемент повысить до каталога.

Инвариант единственного ненулевого Reporter. Каждый модуль, который репортит (Thresher, Instance, EventHub, dispatcher), держит ровно один Reporter, и он никогда не nil — модуль достаёт его через рантайм (EngineRuntime.Reporter() возвращает ненулевой echo-only дефолт, когда более богатый Reporter не установлен) или, для компонента без рантайма (dispatcher), ненулевой дефолт, заданный при конструировании, который движок переопределяет при проводке. Модуль НИКОГДА не решает «логировать или наблюдать» per-call и никогда не откатывается между sink'ом и logger'ом; он держит Reporter и вызывает Report. Logger() остаётся отдельным аксессором, сохранённым строго для диагностики правила 3.

2.8 Два observer-scope'а — instance и engine

Registry v.1 — per-instance (AddObserver на handle) — верно для «следить за моим instance», но структурно неспособен нести события движка, hub'а, процесса или worker-job, которые не принадлежат никакому instance. v.2 добавляет недостающий scope:

  • Instance scope (существует): observer'ы, зарегистрированные на InstanceHandle, получают события этого instance — InstanceState, NodeProgress, gateway/correlation/boundary/task/fault/data-события этого instance.
  • Engine scope (новый): observer'ы, зарегистрированные на движке, получают всё — engine-scope-виды нативно (движок, hub, процесс, job) и каждое instance-scope-событие каждого instance (каждое событие несёт свой instance_id в details, §2.9). Одна подписка = согласованный вид всего движка; listener фильтрует по kind/id, а не жонглирует per-instance-регистрациями.
  • Оба scope'а разделяют одну форму события, один контракт доставки (per-observer буферизованный канал, неблокирующая отправка, счётчик drop'ов, сдерживание паник) и одно read-only-правило.

2.9 Payload события — один словарь атрибутов через оба канала

Событие сохраняет identity-only-форму v.1 (timestamp, id/имя узла, строка phase/state, вид) и обретает плоскую string-to-string details-мапу для kind-специфичных идентификаторов — id job'а, id task'а, выбранные flow шлюза, имя ключа корреляции, код ошибки. Два правила:

  • Ключи details И ЕСТЬ канонический словарь log-атрибутов ADR-022 §2.5 (instance_id, node_id, job_id, task_id, correlation_key/_value, error, …). Один словарь обслуживает оба канала, так что listener и log-дашборд коррелируют по одним именам — а producer §2.7 может писать log-эхо прямо из details события.
  • Маскировка держится: id, имена, состояния, коды — никогда payload-значения (ADR-010/011). DataChange докладывает что именованный элемент изменился и как (added/updated/deleted), никогда что он теперь содержит; listener, которому нужно значение, читает его через read-only-ридер данных, видимо.

2.10 Коррекции к приземлённой минимальной форме v.1

Три дефекта приземлённого минимума становятся требованиями уровня контракта:

  • Разсвернуть node-progress. Приземлённое событие узла проецирует всё на трёхзначное состояние токена; фазы NodeProgress из §2.6 несут реальную, BPMN-названную фазу исполнения (entered/executing/completed/failed/ canceled/merged/parked). Проекция токена остаётся доступной на token view handle — уходит именно свёртка в поток событий.
  • Faults — первоклассные события. BPMN-ошибка, пойманная на boundary, — спроектированный, ожидаемый путь — но она должна быть видимой (Fault: Caught, эхо Debug), а не молчаливой как сегодня; непойманный fault — это Fault: Uncaught с эхом Error (существующая запись instance-fault становится log-эхом этого вида). Thrown завершает тройку, так что listener может следить за ошибкой от подъёма до диспозиции.
  • Изменения данных наблюдаемы — ⏳ отложены до редизайна data-plane. DataChange — именованный вид в таксономии (строка держит listener-контракт), но его эмиссия отложена. Предполагавшимся источником был существующий callback change-notification (Value плоскости данных: added/updated/deleted, асинхронный fan-out) — но модель исполнения frame-clone-then-replace: узел работает на клонированной копии фрейма, а commit фрейма заменяет объект значения scope-контейнера (Scope.Commit: vv[name] = d), так что callback, зарегистрированный на исходном значении, обходится и наблюдает мало или ни одного реального изменения. Вместо того чтобы подключать механизм против плоскости данных, которую вот-вот переработают, наблюдаемость DataChange проектируется вместе с переработкой структурных данных + маппинга (ADR-011) — механизм change-notification, от которого она зависит, сам есть часть этого редизайна. Когда она приземлится, это только observer-поток, без log-эха (на объёме hot-path — грубо десять записей на узел — даже Debug утопил бы трассировку flow; lossy-поток — правильный транспорт, и hot-path-следствие ADR-022 остаётся в силе).

2.11 Политика видимости — hide-by-policy на краю доставки

Идентификаторы и имена сами по себе — информация: correlation_value — бизнес- значение (возможно PII), имена узлов/задач могут быть чувствительны, а engine-scope observer (§2.8) видит каждый instance — в multi-tenant-host'е listener одного тенанта не должен видеть события другого. Структурная маскировка (§2.9) держит payload-значения снаружи; видимость остального — вопрос политики, а политика принадлежит policy-authority движка.

Механизм — домашний паттерн опциональной capability (type assertion против существующего расширения — так же, как работают key-extension hub'а и worker-config capabilities), заякоренный на провайдере авторизации (auth-расширение ADR-002), поскольку «кто что может видеть» — забота авторизации:

  • Видимость лога — если провайдер реализует capability редакции лога (рабочее имя LogRedactor), producer §2.7 пропускает каждое событие через неё перед записью log-эха: политика может пропустить его, отредактировать detail-ключи или подавить запись. Один получатель (лог), одно глобальное решение.
  • Видимость observer'а — если провайдер реализует capability фильтра наблюдения (рабочее имя ObservationFilter), каждое событие проверяется per-recipient перед доставкой: пропустить как есть, доставить с отредактированными details или запретить (observer никогда не видит событие; счётчики drop'ов не инкрементируются — запрещённое событие есть политика, не backpressure).
  • Не реализовано ⇒ pass-through. Нет capability, нет политики: события доходят до лога и observer'ов ровно как произведены. Дефолт не стоит ничего — capability проверяется assertion'ом один раз при проводке (старт движка / регистрация observer'а), никогда per-event.

Сама модель политики — идентичности, тенанты, классификации атрибутов, per-kind-правила — намеренно не проектируется здесь: она приходит с multi-tenancy/IAM-работой, реализованной на том же auth-расширении. Этот ADR фиксирует seam, его якорь и его zero-cost-дефолт; точные имена интерфейсов и сигнатуры подтверждаются wiring-SRD.

2.12 Non-goals и scope (phased core; каждое отложение названо)

  • Мутирующие per-node-listener'ы (observer, который ставит переменную, перенаправляет flow, ветирует переход) — отвергнуты по принципу (§4), не отложены. Изменения поведения идут в модель, видимо.
  • Мелкозернистое управление исполнением / step-debugger (single-step токена, breakpoint'ы) — вне scope; управление грубое (instance-level).
  • Реализация отложенных состояний жизненного цикла (Failing/Paused/ Compensating и их подсистемы) — принадлежит ADR error-handling, suspend и compensation; этот ADR лишь резервирует их имена/слоты в открытом словаре, чтобы они расширялись аддитивно.
  • Механика shutdown'а горутин waiter'ов (2.5) — ADR-006; этот ADR решает публичный контракт Shutdown, который ею управляет.
  • Персистентность / durable-история (запрос завершённых instance после рестарта) — persistence ADR; канал и handle здесь живые/in-memory.
  • Разбиение god-object'а Instance (audit 2.3) — приземлено как родственный рефакторинг; больше не открыто.
  • Точные формы интерфейсов handle/observer/control и представление токенов/прогресса — реализационные решения для SRD(ов), staged green.
  • Проводка таксономии v.2 — эмиссия каждого вида §2.6 через producer §2.7, engine-scope registry, details-мапа payload'а, capabilities видимости §2.11 и три коррекции §2.10 — едет с сопровождающим SRD; этот ADR фиксирует контракт.
  • Модель политики видимости (идентичности, тенанты, классификации атрибутов, per-kind-правила) — принадлежит multi-tenancy/IAM-работе, реализованной на том же auth-расширении, к которому якорится §2.11; этот ADR лишь фиксирует seam capability и его pass-through-дефолт.
  • Экспорт метрик/трейсинга тех же событий (мост OpenTelemetry поверх engine-scope observer'а) — естественный потребитель этого контракта, отложен в собственную работу.

3. Последствия

  • Публичный API перестаёт быть write-only. Host следит за состоянием, движением токенов, прогрессом узлов, данными и исходом вживую — и может отменить — вместо чтения логов (2.2 закрыт).
  • Один seam для всех узлов. Каждый узел/задача докладывает прогресс в один канал; исполнители ADR-014 и будущие виды включаются туда, не в отдельные пути.
  • Наблюдаемость переживает эволюцию состояний. Стандартно-названный, открытый словарь + forward-compatible-потребители означают, что отложенные состояния приземляются аддитивно — без churn'а публичного API (опасение §1.4 разрешено by design).
  • Управление безопасно и видимо. Грубые, опосредованные движком cancel/suspend не могут испортить исполнение; скрытое per-node-управление не вводится.
  • Движок обретает реальный жизненный цикл. Shutdown (форсирующий вопрос waiter'ов 2.5) и UnregisterProcess (чинящий утечку).
  • Новая публичная поверхность для поддержания стабильной под ADR-002 §4.7 — handle, канал и операции управления; держится узкой и forward-compatible.
  • Цена: проекция + канал, протянутый через track, + проводка cancel. Ограничена констрейнтами read-only-наблюдения + грубого управления; SRD стейджит это, и узлы перенимают доклад прогресса инкрементально.
  • (v.2) Два канала не могут разойтись. Один producer на переход пишет и log-эхо, и observer-событие — mirror-by-construction, одна точка ревью на строку каталога, и правило handle-once ADR-022 расширяется естественно (вызов Observe() И ЕСТЬ единственная обработка репортинга этого перехода).
  • (v.2) Listener'ы становятся реальной платформой. С полной таксономией §2.6 и engine-scope, audit-трейлы, UI, интеграционные мосты и будущий экспорт OpenTelemetry — всё висит на одном стабильном контракте, а не скрейпит логи.
  • (v.2) Цена: engine-level registry + одна эмиссия на строку каталога. Per-event-работа ограничена (мапа id + неблокирующая отправка); вид высокой частоты (DataChange) намеренно пропускает log-эхо, а места эмиссии — ровно те места, которые ремедиация ошибок-и-логирования уже посетила: хорошо известный, недавно тронутый код.

4. Рассмотренные альтернативы

  • Заморозить закрытый enum состояний в публичном API сейчас. Просто и типизированно. Отклонено: наши состояния неполны (§1.4); закрытый enum churn'ит, когда приземлятся Failing/Paused/Compensating. Стандартно-названный открытый словарь (§2.4) даёт ту же ясность, остаётся стабильным и расширяется аддитивно.
  • Отложить весь механизм, пока жизненный цикл не станет полным по стандарту. Мой первый инстинкт. Отклонено (верно оспорено): это оставляет host'ы слепыми надолго (отложенные подсистемы далеко), и заставляет каждый будущий узел ретрофитить наблюдением позже — дорогой порядок. Seam должен существовать первым; словарь врастает в него.
  • Camunda-style мутирующие execution/task-listener'ы. Мощные, но они ровно невидимое, вне-диаграммное управление, которое ADR-011 запрещает; поведение процесса зависело бы от зарегистрированного кода, который читатель диаграммы не видит. Отклонено — наблюдение read-only; операторское управление явно и грубо (§2.3).
  • Вернуть сырой *Instance / выставить internal/instance. Утекает god-object (2.3), его мутирующие методы и дисциплину локов; host мог бы испортить работающий instance. Отклонено в пользу узкого handle.
  • Только polling (без канала). Не может поймать транзиентные token-movement / node-события между poll'ами и жжёт циклы. Отклонено как единственный механизм (polling через handle остаётся доступным для простых случаев).
  • Свернуть в metrics/tracer-расширения (ADR-002). Агрегатная телеметрия — иная нужда, чем per-instance-состояние/управление + lifecycle-callback'и. Комплементарно, не замена.
  • (v.2) Держать две независимые эмиссии на переход — вызов лога И вызов observer'а, синхронизированные конвенцией («правило зеркалирования»). Первый черновик v.2. Отклонено на ревью: это удваивает каждую точку вызова, а конвенция — это ровно то, что позволило двум каналам разойтись изначально (состояния instance наблюдаемы-но-не-логируемы, worker-события логируемы-но-не-наблюдаемы). Единый producer делает зеркало структурным.
  • (v.2) Плоский per-transition event-enum (одно значение на строку каталога, ~40+ значений). Отклонено: хрупко и вечно-растущее; модель kind + открытая фаза соответствует дисциплине словаря §2.4 и позволяет обеим осям расширяться аддитивно.
  • (v.2) Слить каналы — доставлять log-записи observer'ам или логи через observer'а. Отклонено: контракты надёжности различаются by design (ADR-022 §2.7) — логи не должны становиться lossy, а поток не должен становиться блокирующим; унифицируется только producer.
  • (v.2) Логировать всё вместо расширения observer'ов (полнота на одном log-канале). Отклонено: оставляет программные listener'ы слепыми — логи для операторов; платформа listener'ов — это поток.

5. Рекомендации по enterprise-готовности

Совещательно, не блокирующе — для реализующего SRD(ов):

  • Маскировать payload'ы в каждом событии (только имена/id/состояния/ timestamp'ы), по ADR-010/011.
  • Содержать отказы observer'ов (recover/timeout/drop-with-warning), чтобы observer никогда не мог застопорить или уронить track.
  • Сделать потребителей forward-compatible — документировать, что множество состояний/видов открыто; host должен терпеть неизвестные значения (так аддитивный рост никогда его не сломает).
  • Сделать Cancel/Shutdown идемпотентными и ограниченными ctx; документировать, что происходит с in-flight-instance'ами по дедлайну Shutdown.
  • Возвращать result завершения (терминальное состояние + error/incident) из WaitCompletion, не только состояние.
  • (v.2) Спаривать capabilities видимости (§2.11) с нуждами комплаенса: PII/GDPR-редакция на log-канале через LogRedactor + JSON-handler, tenant-изоляция на engine-scope observer'е через ObservationFilter; документировать, что запрещённое событие невидимо по политике, не потеряно из-за backpressure (счётчик drop'ов остаётся честным).

6. Открытые вопросы

  • Нет. v.1 решила механизм (handle + канал + грубое управление + контракт асинхронной lossy доставки + открытый словарь). v.2 решает покрытие: каноническую таксономию (§2.6 — виды, фазы, scope'ы, уровни log-эха, зарезервированные слоты), единый producer поверх двух всё-ещё-раздельных каналов (§2.7), engine-scope registry, получающий всё (§2.8), details-мапу, ключённую словарём ADR-022 §2.5, с сохранённой маскировкой (§2.9), три коррекции к приземлённому минимуму — развёрнутые фазы узла, первоклассные faults, изменения данных через существующий callback без log-эха (§2.10) — и seam политики видимости (опциональные capabilities на auth-расширении, pass-through-дефолт; §2.11). Точные формы типов, форма API эмиттера, размеры буферов и стейджинг мест эмиссии — реализационные заботы для сопровождающего(их) SRD(ов).

7. Ссылки

  • SAD-001 v.1 Vision & Architecture — цель «библиотека, встроенная в host», которая делает наблюдаемость/управление базовой ставкой.
  • ADR-001 v.6 Execution Model — жизненный цикл instance/track, чьи состояния этот ADR называет по стандарту и чьи отложенные состояния (Failing/Paused, §4.2 §9) открытый словарь резервирует слотами.
  • ADR-002 v.2 Extension Architecture — движковый каталог расширений (§4.2), через который регистрируются observer'ы; версионирование публичного API §4.7, к которому присоединяется эта поверхность.
  • ADR-006 v.2 Events & Subscriptions — жизненный цикл waiter'а (2.5), который Shutdown должен закрыть; sibling.
  • ADR-022 v.1 Error Propagation and Logging Policy — уровни log-канала (§2.4), словарь атрибутов (§2.5) и разделение двух каналов (§2.7), в которые пишет единый producer этого ADR и которое он уточняет до «раздельные каналы, единый producer».
  • ADR-010 v.2 Process Data Model — плоскость данных (свой лок → безопасные внешние чтения) + §2.7, чьё представление чтения данных переиспользует handle; отложил «observe from outside» сюда.
  • ADR-011 v.5 Process Data Flow — принцип no-hidden-control, которому следует правило read-only-observer'а; публичная поверхность чтения, которую зеркалит handle.
  • ADR-012 v.1 Execution Layering — поверхность публичного контракта, которую расширяют этот handle/канал (конкурентный sibling).
  • ADR-014 v.1 Message Handling — SendTask/ ReceiveTask докладывают прогресс в lifecycle-канал этого ADR.
  • BPMN 2.0 §13.3.2 (Activity lifecycle, spec p428–429) + §13.2/§13.5.6 (process lifecycle) — имена состояний, по которым этот ADR выравнивается (и расширяет по мере приземления подсистем); дайджест в docs/bpmn-spec/state-machines/.
  • Архитектурный аудит 2026-06-11 (docs/audit/architecture-audit-2026-06-11.md) — находка 2.2 (write-only API; утечка Shutdown/UnregisterProcess/snapshots); касается 2.5 (жизненный цикл waiter'а, ADR-006).

История документа

Версия Дата Автор Изменение
v.2 2026-07-11 Руслан Габитов Accepted (приземлено сопровождающим wiring-SRD). Полнота наблюдаемости — один event-seam, на весь движок. Приземлённая форма v.1 была намеренно минимальной (два вида observer-событий, только instance-scope); аудит полноты обоих каналов показал, что observer-поток и operator-логи ADR-022 покрывают каждый разную половину движка, с некоторыми переходами (решения шлюзов, взятие task, арминг/дизарминг boundary, boundary-пойманные faults), молчащими на обоих. v.2 добавляет: каноническую таксономию наблюдаемых событий (§2.6 — 13 видов через engine/hub/process/instance/node/gateway/event/correlation/job/task/boundary/fault/data, каждый с открытым множеством фаз, scope'ом, уровнем log-эха и ⏳ зарезервированными слотами для pause/resume/incident/dehydration) с правилом полноты эмиссии (не-эмитированный переход каталога есть дефект); единый producer — один вызов в стиле Report(fact) на переход, который пишет log-эхо И питает observer-поток, уточняя ADR-022 §2.7 до раздельные каналы, единый producer (отвергает «правило зеркалирования» из двух независимых эмиссий, которое позволило каналам разойтись); engine-scope observer registry (получает всё, вкл. события всех instance — один согласованный вид gobpm) наряду с instance-scope из v.1; details-мапу, ключённую каноническим словарём атрибутов ADR-022 §2.5 (один словарь через оба канала; маскировка цела — id/имена/коды, никогда payload-значения); и три коррекции к приземлённому минимуму — развёрнутые фазы node-progress (3-значная проекция токена остаётся на handle, не в потоке), первоклассный Fault (Thrown/Caught/Uncaught — boundary-пойманная ошибка становится видимой) и DataChange (⏳ отложен в переработку плоскости данных ADR-011 — его словарь приземлён, проводка едет с той переработкой; только observer-поток, без log-эха, когда приземлится). Добавляет seam политики видимости (§2.11): опциональные capabilities на auth-расширении — редакция лога (рабочее имя LogRedactor) и per-recipient-фильтрация наблюдения (ObservationFilter), обнаруживаемые type assertion'ом при проводке; не реализовано ⇒ pass-through (события доходят до лога/observer'ов как есть, нулевая цена); модель политики (идентичности/тенанты/классификации) отложена в multi-tenancy/IAM-работу на том же расширении. Non-goals обновлены (разбиение god-object'а приземлено; мост OTel + модель политики отложены); ссылки перепинены (ADR-001 v.5→v.6, ADR-006 v.1→v.2, добавлен ADR-022 v.1). Концепция; проводка таксономии едет с сопровождающим SRD.
v.1 2026-06-18 Руслан Габитов Принято. Исправляет audit 2.2, строя механизм наблюдения-и-управления: публичный InstanceHandle (состояние, движение токенов, прогресс исполнения узлов, read-only-данные через публичный ридер (ADR-011 v.5), WaitCompletion, Cancel сейчас / Suspend·Resume зарезервированы), один lifecycle-канал, в который узлы/задачи публикуют прогресс (seam, в который включается будущая работа по узлам/задачам), и жизненный цикл движка (Shutdown, UnregisterProcess, чиня утечку snapshots). Согласует неустоявшийся вопрос состояний: механизм — стабильный контракт, тогда как словарь состояний/узлов назван по стандарту BPMN, но оставлен открытым множеством — выровнять имена сейчас, расширять аддитивно по мере приземления подсистем Failing/Paused/Compensating (без churn'а публичного API; потребители forward-compatible). Observer'ы read-only над данными/flow (без мутирующих listener'ов — скрытое управление, ADR-011); управление жизненным циклом грубое, явное, опосредованное движком. Асинхронная доставка (stdlib): best-effort lossy per-observer буферизованный канал + горутина дренажа + неблокирующая отправка + счётчик drop'ов — track никогда не блокируется на observer'е; только терминальное завершение — гарантированный, блокируемый сигнал (закрытый done-канал через WaitCompletion). Phased core: отложенные состояния/подсистемы жизненного цикла, мелкозернистое step-управление, механика waiter-shutdown (2.5 → ADR-006), персистентность/история и разбиение god-object'а Instance (2.3) вне scope. Уточняет ADR-002 v.2; siblings ADR-001 v.5, ADR-006 v.1 (Принято), ADR-010 v.2, ADR-011 v.5, ADR-012 v.1, ADR-014 v.1. Принято на v.1 с коррекциями пинов/standard-claim при принятии: ADR-002 v.1→v.2; activity-lifecycle §13.2.2→§13.3.2 (KB атрибутирует его §13.3.2, p428–429). Концепция; реализация едет с SRD.