ADR-001 — Модель исполнения: Instance + Tracks¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.6 |
| Дата | 2026-06-27 |
| Владелец | Руслан Габитов |
| Замещает | v.2 (трёхслойная модель Instance / track / token). v.3 сворачивает её до двух слоёв — token становится логической проекцией track'а, а не хранимой сущностью — и откладывает персистентность / регидрацию в отдельный ADR. |
| Уточняет | SAD-001 v.1 §10 Execution Model |
EN-оригинал — канонический: ADR-001-execution-model.md. Этот файл — его перевод (twin).
1. Контекст¶
goBpm исполняет процессы BPMN 2.0 по модели token-flow из спецификации. In-memory рантайм, который определяет этот ADR, ОБЯЗАН обрабатывать:
- Множество параллельных Process Instance (один движок, N экземпляров разных определений процессов исполняются параллельно).
- Множество параллельных потоков исполнения внутри одного instance (split на Parallel Gateway, Multi-Instance Activity, активации Event-Sub-Process).
- Кардинально разные временные масштабы в одном instance — ServiceTask за миллисекунды; UserTask, ждущий днями; многодневный Timer. Длинные ожидания НЕ ДОЛЖНЫ удерживать горутину.
- Предсказуемую мутацию состояния без гонок данных — переходы соответствуют нормативным конечным автоматам BPMN (activity-lifecycle.md).
- Корректное завершение (Terminate End Event отменяет всю выполняющуюся работу в instance, end-events.md).
Область этого ADR. Он определяет ядро модели исполнения рантайма — как instance и его потоки исполнения живут, форкаются и отменяются в памяти. Концерны, надстраивающиеся над этим ядром, принадлежат отдельным ADR и здесь не специфицируются (см. §9): семантика join/merge (ADR-005), доставка событий и отмена по событию (ADR-006), in-memory модель освобождения на длинном ожидании (ADR-007). Персистентность и восстановление — first-class P0 требование (SAD-001 §6), отдельный тяжёлый концерн, отложенный в выделенный Persistence & State ADR; §4.7 фиксирует только инварианты уровня рантайма, которые персистентность обязана сохранить.
Текущая кодовая база уже содержит развитую модель в internal/instance/ (Instance + track + token + stepInfo). Этот ADR формализует рантайм, которому служат эти типы, и сворачивает token из хранимого типа в проекцию — см. §3.1 и §6.
2. Решение¶
Двухслойное владение. Instance владеет одним или несколькими track'ами. track — операционный поток исполнения (одна горутина), несущий свою текущую позицию во flow и состояние. token — контрольная позиция из спецификации BPMN, выраженная как логическая проекция текущего шага track'а (track.Token(), Instance.GetTokens()), а не хранимый объект. Instance держит только реестр track'ов — реестра токенов нет; «instance завершён» означает, что все track'и закончились. Состояние уровня instance мутируется только внутри единственной горутины event-loop'а Instance; track'и сообщают о прогрессе событиями в канал и никогда не мутируют состояние Instance напрямую. Форк создаёт новый track на каждую дополнительную ветку (1:1 между track'ом и его позицией), родительский track продолжает по первому flow. context.Context — контракт отмены. Персистентность и регидрация специфицируются отдельно; этот ADR — in-memory рантайм, на котором они строятся.
В сводной форме:
| Концерн | Механизм |
|---|---|
| Владение состоянием instance | Instance регистрирует и координирует track'и; мутирует состояние instance только в своём сериализованном event loop |
| Исполнение потока | track работает в выделенной горутине; ведёт конечный автомат шага по последовательности узлов |
| Семантическая позиция BPMN | token — проекция текущего шага track'а (позиция + производное состояние + происхождение); вычисляется по запросу, не хранится |
| Механика форка | родительский track продолжает по первому flow; один новый track на каждый дополнительный flow, каждый в своей горутине |
| Безопасность мутации состояния | состояние instance мутируется только внутри его event-loop-горутины (без блокировок); состояние track'а — только внутри его собственной горутины |
| Отмена | каскад context.Context: Engine → Instance → track |
| Происхождение (lineage) | хранится на track'е (track.prev); родительство при форке — это родительство track'а |
| Join / события / длинные ожидания | вне области здесь — ADR-005 / ADR-006 / ADR-007 (см. §9) |
| Персистентность / рестарт | отложено — выделенный Persistence & State ADR |
3. Рассмотренные альтернативы¶
3.1 Альтернативы слоёв¶
| Вариант | Описание | Вердикт |
|---|---|---|
| Один слой (Instance + token) | У каждого токена своя горутина и конечный автомат. | Отклонено. Теряется декомпозиция стадий исполнения (prologue / execute / epilogue / await-results), которую несёт модель stepState; перегружает токен и семантикой BPMN, и управлением горутиной. |
| Три слоя (Instance + track + token-как-объект) | Track владеет горутиной + автоматом шага; token — отдельный живой объект, владеющий позицией BPMN + происхождением, 1:1 с track'ом. | Отклонено (был выбор v.2). При 1:1 без миграции живой token-объект лишь дублирует track: вынуждает двунаправленные обратные ссылки (token.inst, token.trk), второй реестр (Instance.tokens[] рядом с tracks) и дублирующую цепочку происхождения (token.prevs/nexts, зеркалящую track.prev). Отдельная идентичность токена нужна только как сериализуемое значение во время персистентности — это решает Persistence ADR — а не как живой конкурентный объект. |
| Два слоя (Instance + track; token как проекция) — выбрано | Track — операционный поток, несущий позицию + состояние; token — read-model, проецируемый из текущего шага track'а. | Выбрано. Убирает клубок перекрёстных ссылок и дублирующие реестры/происхождение; инвариант 1:1 держится по построению (нечего синхронизировать). Token выживает как словарь BPMN и как проекция/сериализуемое значение, а не как хранимая сущность. |
| Четыре слоя (явный Scope как уровень владения) | Scope как слой владения между track'ом и позицией. | Отклонено. Scope (internal/scope/) — контекст разрешения данных, а не уровень владения. |
3.2 Механика форка¶
| Вариант | Механика | Вердикт |
|---|---|---|
| A. Форк держит N позиций на одном track'е | Одна горутина ведёт параллельные ветки последовательно | Отклонено. Нет реального параллелизма. Текущий token.split(n) держит split на том же track'е — именно это поведение меняет v.3. |
| B. Форк создаёт новый track на каждую доп. ветку — выбрано | Каждая параллельная ветка получает свою горутину; родитель продолжает по первому flow | Выбрано. Истинный параллелизм; соответствует «конкурентным потокам исполнения» BPMN; стоимость горутины пренебрежимо мала. |
| C. По политике (track'и или позиции) | Instance выбирает | Отклонено. Поверхность политики без выигрыша; ограничения конкурентности можно навесить семафором над вариантом B. |
3.3 Мутация состояния instance¶
| Вариант | Механика | Вердикт |
|---|---|---|
| Общее состояние + mutex | Track'и держат указатель на Instance и мутируют поля под блокировкой | Отклонено. Класс багов lock-дисциплины; зависимость от race-детектора; конкуренция при сильном fan-out. (Это форма текущего кода.) |
| Сериализация через event-loop — выбрано | Instance владеет своим состоянием; track'и шлют события в канал; одна горутина Instance применяет их по порядку | Выбрано. Мутация одним владельцем — без гонок по построению, без блокировок на состоянии instance. |
3.4 Драйвер горутин¶
| Вариант | Вердикт |
|---|---|
| OS-процесс на instance | Отклонено — стоимость старта, IPC, ломает встраиваемость. |
| Один глобальный реактор на все instance | Отклонено — один медленный узел блокирует все instance. |
| Горутина-на-track — выбрано | Нативная конкурентность Go; соответствует модели конкурентных потоков BPMN. |
4. Детали решения¶
4.1 Роли¶
Instance¶
Граница владения. Одна горутина крутит event loop; она — единственный писатель состояния instance.
- Держит реестр живых track'ов (карта активных потоков исполнения). Реестра токенов нет.
- Принимает track-события (прогресс / форк / завершение) в канал и применяет их по порядку.
- Порождает новые track'и в точках форка.
- Владеет корневым
context.Context; отмена каскадируется на все track'и. - Решает завершение instance: instance завершён, когда все track'и закончились (в реестре не осталось активных), либо при отмене контекста / остановке.
- Проецирует токены по запросу:
Instance.GetTokens()возвращает токен-вид каждого активного track'а.
track¶
Один поток исполнения. Одна горутина на track на всё его время жизни.
- Несёт свою текущую позицию во flow (узел, на котором стоит) и состояние уровня track + уровня шага.
- Ведёт жизненный цикл шага для текущего узла: prologue → execute → epilogue → await results.
- Исполняет поведение BPMN узла через его
NodeExecutor. - Читает входы из Scope, пишет выходы обратно; эмитит track-события Instance на каждом наблюдаемом BPMN-переходе.
- Несёт происхождение форка (
track.prev). - Обрабатывает прерывание через
ctx.Done(); чисто завершается при завершении, ошибке, merge или отмене. - Выставляет свою позицию как проекцию токена:
track.Token().
token (проекция — не хранимый тип)¶
Token — контрольная позиция управления из спецификации BPMN, выраженная как read-model, проецируемый из текущего шага track'а — позиция узла + производное состояние + происхождение. Вычисляется по запросу (track.Token(), Instance.GetTokens()), никогда не хранится как живой перекрёстно-связанный объект и не имеет обратной ссылки на свой track или на Instance. «token» остаётся словарём проекта и BPMN (события, аудит и — позже — единица, сериализуемая персистентностью, именуются токеном); ушёл только живой объект.
4.2 Конечные автоматы¶
Жизненный цикл instance: Created → Active → Completed, с веткой отмены Active → Terminating → Terminated (instance.State). Ветка ошибок (Failing/Failed) и приостановка (Paused) принадлежат своим будущим ADR и намеренно отсутствуют в рантайме (см. §9).
Жизненный цикл track'а (сохранён из текущего кода):
TrackCreated → TrackReady → TrackExecutingStep → TrackProcessStepResults → TrackWaitForEvent
↓
[TrackMerged | TrackEnded | TrackCanceled | TrackFailed]
Рантайм производит терминальные состояния track'а TrackEnded / TrackCanceled / TrackFailed. TrackMerged (синхронизирующий join) и причину завершения withdrawn (проигрыш гонки на Event-Based Gateway) производит ADR-005; их значения enum существуют, но в этом ядре не имеют производителя.
Жизненный цикл шага (сохранён):
StepCreated → StepStarted → StepPrologued → StepExecuting → StepEpilogued → StepAwaitsResults → StepEnded → (StepFailed)
Состояние токена как проекция. Состояние токена не хранится; это чистая функция от состояния track + шага:
| Token (логически) | Проецируется из (tokenStateFor) |
|---|---|
Alive |
TrackReady / TrackExecutingStep / TrackProcessStepResults |
WaitForEvent |
TrackWaitForEvent |
Consumed |
TrackEnded / TrackMerged / TrackCanceled / TrackFailed |
~~Withdrawn~~ |
снято — Event-Based gateway маршрутизирует без создания токенов на плечах, поэтому токен Withdrawn не производится (ADR-005 v.4 §2.12.1) |
Это заменяет хранимый enum TokenState из v.2 производным видом. (Значение TokenWithdrawn когда-то было зарезервировано под проигрыш гонки на Event-Based Gateway; ADR-005 v.4 §2.12.1 сняло его — шлюз маршрутизирует, никогда не помещая токен на проигравшее плечо, так что проецировать Withdrawn не из чего.)
4.3 Топология каналов (event loop)¶
Построенная форма — один входящий поток событий (track → Instance):
type Instance struct {
ctx context.Context
events chan trackEvent // tracks -> loop() (evFork / evEnded)
tracks map[string]*track // мутируется ТОЛЬКО в loop()
state atomic.Uint32 // run-состояние; пишется только loop(), читается lock-free
// ... без реестра токенов
}
func (i *Instance) loop(ctx context.Context, initial []*track) {
// порождает начальные track'и, затем дренаж пока все track'и не закончатся:
for active > 0 {
select {
case <-ctx.Done():
stopAll() // сигнал каждому track'у; loop продолжает дренаж
case ev := <-i.events: // evFork -> построить track на каждый доп. flow
... // evEnded -> active--
}
}
// все track'и закончились -> Completed, либо Terminated, если завершение вызвала отмена
}
Второе входящее ребро (EventHub → Instance для доставки Message / Timer / Signal) добавляет ADR-006; оно не часть этого ядра. Наблюдаемый BPMN, токен-ориентированный вид (split / merged / waiting / consumed / withdrawn) выводится из этих событий для аудита (и, позже, для чекпойнтов персистентности) — это не второй живой канал.
4.4 Механика форка¶
Точка форка — любой FlowNode с N>1 исходящими sequence flow, которые становятся активными — не только эксклюзив-шлюз (Activity с несколькими исходящими flow — это неуправляемый split, token-flow.md).
- Track A исполняет узел форка; его активные исходящие flow — F1…FN (в порядке объявления).
- Track A продолжает по F1 — его позиция переходит к цели F1; A не заканчивается.
- На каждый оставшийся Fₖ (k=2…N): Instance конструирует новый track на узле-цели Fₖ, с
track.prev = A(происхождение), регистрирует его и запускает его горутину. - После форка N track'ов работают независимо — 1 исходный + N−1 новых — каждый со своей позицией, каждый в своей горутине.
Какие исходящие flow активируются по типу шлюза (parallel / inclusive split / неуправляемый split активности), определяет ADR-005; это ядро форкается по тем flow, которые узел сообщает активными.
4.5 Механика join — вне области¶
Семантика join/merge (синхронизирующий join, несинхронизирующий merge, OR-join, Event-Based Gateway) не часть этого ядра рантайма. Она определена в ADR-005 Gateways & Joins. Сегодня в рантайме нет учёта join: узел, до которого дошли несколько track'ов, исполняется по разу на каждое прибытие.
4.6 Каскад отмены через context¶
- Контекст Engine владеет всеми контекстами Instance; контекст Instance производен от него; контекст track'а — от контекста Instance.
- Остановка Engine → отмена всех контекстов Instance → каскад.
- При отмене loop сигналит каждому track'у (
stop()), продолжает дренировать их терминальные события и достигаетTerminated, когда все вышли; нормальный дренаж достигаетCompleted.
Узлы BPMN, запускающие этот каскад — Terminate End Event (отменяет весь instance) и прерывающие boundary-события (отменяют один track) — принадлежат ADR-006; это ядро владеет механизмом каскада, а не его BPMN-триггерами.
4.7 Инварианты рантайма для длинных ожиданий и персистентности¶
In-memory модель освобождения на длинном ожидании (горутина ждущего track'а заканчивается; при приходе триггера порождается свежий track) принадлежит ADR-007 In-Memory Long Waits, а долговечная версия (переживающая рестарт) — Persistence & State ADR. Это ядро фиксирует только инварианты рантайма, которые обе обязаны соблюдать:
- Состояние продолжения track'а полностью описывается его позицией (узлом), состоянием track/шага, данными Scope и происхождением — нет скрытого состояния на отдельном объекте-токене.
- Узел с возобновляемым in-flight состоянием (позиция таймера, подписка корреляции, частичное состояние активности) владеет формой этого состояния. Владение состоянием рантайма решено в ADR-009 v.1: каждый instance клонирует шаблон процесса в свой собственный приватный граф узлов, так что состояние рантайма на каждый instance живёт на собственном узле этого instance — разделяемые определения шаблона остаются неизменяемыми. Долговечная персистентность этого состояния — его сериализация и регидрация через рестарт — остаётся концерном Persistence & State ADR.
5. Последствия¶
Плюсы
- Верность словарю BPMN без накладных расходов. Token остаётся концептом спецификации; track — операционный примитив; токен — просто спроецированная текущая позиция track'а.
- 1:1 по построению. Нет второго реестра, нет дублирующего происхождения, нет обратных ссылок token↔track/instance для синхронизации.
- Состояние instance без гонок. Один владелец-event-loop; без блокировок на состоянии instance; race-детектор (теперь гейтит CI) это подтверждает.
- Нативно для Go. Горутины + каналы + контексты; без фреймворка.
- Переиспользует существующую структуру.
track,stepInfo,trackState,stepStateпереносятся; работа — в удалении типа token и реактивных/lock-путей, а не в изобретении машинерии.
Минусы / что нужно соблюдать
- Число горутин = сумма активных track'ов. Ограничено структурой BPMN; патологические модели можно ограничить семафором над путём форка (оптимизация, не смена модели).
- Дисциплина каналов обязательна. Каждый track ОБЯЗАН эмитить терминальное событие до выхода своей горутины, иначе утечка. Митигация:
defer-очистка; тесты проверяют, чтоruntime.NumGoroutine()возвращается к базовой линии. - Нет справедливости между track'ами. Порядок решает планировщик Go; тесты token-flow НЕ ДОЛЖНЫ зависеть от порядка планирования горутин.
- Терминальные события всегда должны доставляться.
evEndedtrack'а учитывается даже во время отмены (loop дренирует до терминального состояния);emitотбрасывает событие только после выхода loop'а. (Само различие причин withdrawn/canceled приходит с ADR-005.)
6. Концепция vs текущий код — намеренные расхождения¶
Только рантайм (расхождения по персистентности уходят в Persistence ADR). Реализация приземляется по SRD этого рефакторинга.
| Тема | Текущий код | Этот ADR (v.3) | Требуемое изменение |
|---|---|---|---|
| Token как тип | структура token с inst, trk, prevs, nexts, state; Instance.tokens []*token |
Хранимого токена нет; token — проекция текущего шага track'а | Удалить тип token и Instance.tokens. Добавить track.Token() / Instance.GetTokens(), возвращающие вычисленное значение Token (read-model). |
| Кардинальность track:token | token.split(n) делает N токенов на том же track'е (newToken(t.inst, t.trk)), затем checkFlows переназначает |
Track и есть одна позиция; форк делает новые track'и | Убрать split; форк конструирует новые track'и прямо на цели каждого доп. flow. |
| Владение Instance ↔ token | Instance.addToken / tokenConsumed; обратная ссылка token.inst; token.updateState вызывает наверх в Instance |
Instance держит только track'и; обратных ссылок токена нет | Убрать addToken / tokenConsumed / token.inst. «Instance завершён» = все track'и закончились (реестр пуст/терминален), решается в event loop. |
| Происхождение | дублировано: track.prev и token.prevs/nexts |
Единая цепочка на track'е | Оставить track.prev; убрать происхождение токена. |
| Мутация состояния | реактивные методы на Instance под sync.RWMutex |
Одна горутина event-loop; track'и шлют события; без блокировок на состоянии instance | Добавить Instance.loop() + топологию каналов; перевести прямые мутации в события. |
| Состояние токена | хранимый enum TokenState на токене |
Производная проекция из состояния track/шага (tokenStateFor) |
Сделано — enum убран из токена; вычисляется в проекции. Причина завершения withdrawn перенесена в ADR-005. |
| Жизненный цикл instance | 9 ad-hoc состояний (Created/Ready/StartingTracks/Runned/Stopping/Paused/FinishingTracks/Finished/Canceled), часть не используется |
Created → Active → Completed; ветка отмены Terminating → Terminated |
Сделано — enum приведён к словарю §4.2; ошибки (Failing/Failed) и приостановка (Paused) перенесены в свои будущие ADR (§9). |
Известная проблема (перенесена). Исполнение узла вызывает
NodeDataLoader.RegisterData, который сейчас мутирует разделяемый узел (например,EndEvent.dataPath) — нарушение неизменяемости из §4.7, дающее гонку, когда два track'а проходят один узел (вскрыто тестом несинхронизирующего merge). Исправление — контракт состояния/загрузки данных на узел, принадлежащий Persistence & State ADR; поэтому несинхронизирующий merge над разделяемым узлом отслеживается в ADR-005, а не заявляется гейтом §7 этого ядра.
7. Верификация¶
Как мы знаем, что реализация соответствует концепции — приёмочный гейт ядра рантайма. Все строки ниже проверены и зелёные (тесты в internal/instance/, прогон под -race в CI); это доказательство, стоящее за статусом «Принято».
| Что | Как | Чем проверено |
|---|---|---|
| Свобода от гонок | Все тесты под -race (гейт CI). Состояние instance мутируется только в loop(); любая гонка — блокирующий провал CI. |
весь пакет под -race |
| Без утечки горутин | Хелпер проверяет, что runtime.NumGoroutine() возвращается к базовой линии после завершения. |
leakcheck_test.go |
| Форк 1:1 | Split на 2: независимые track'и, каждый со своей позицией; родитель продолжил по F1 (не закончился); ребёнок ответвился после форка. | TestM4ForkCompletes |
| Нет реестра токенов | Токены доступны только через GetTokens() (проекция из track'ов), поля tokens нет; track.Token() отражает текущий шаг. |
TestTokenStateProjection, TestM3* |
| Завершение instance | Instance достигает Completed ровно когда все track'и закончились — не сканированием живых токенов. |
TestM2LinearCompletes, TestM4ForkCompletes |
| Каскад завершения | Отмена контекста останавливает каждый track и дренирует его горутину в пределах границы; instance достигает терминального состояния. | TestTerminationCascade |
Перенесённые гейты принадлежат своим ADR: синхронизирующий join / несинхронизирующий merge → ADR-005; in-memory длинное ожидание → ADR-007; триггеры Terminate End Event / boundary → ADR-006. Тесты восстановления после рестарта и долговечных чекпойнтов принадлежат Persistence & State ADR.
8. Ссылки¶
- SAD-001 v.1 Vision & Architecture — §6 Quality Attributes; §10 Execution Model (этот ADR уточняет); §13 Distribution & Scale (предварительно).
- docs/bpmn-spec/state-machines/activity-lifecycle.md, process-lifecycle.md — нормативные жизненные циклы.
- docs/bpmn-spec/semantics/token-flow.md, gateways.md, end-events.md — семантика fork/join/завершения.
- ADR-005 v.4 Gateways & Joins, ADR-006 v.1 Events & Subscriptions, ADR-007 v.1 In-Memory Long Waits — концерны, уточняющие это ядро (см. §9).
- Persistence & State ADR (будет написан) — политика чекпойнтов, контракт состояния на узел, долговечность длинных ожиданий, восстановление после рестарта, состояние Scope/таймера/компенсации/ошибок/активности. Зависит от интерфейса
Repository(ADR-002 v.1 Extension Architecture). - Существующий код:
internal/instance/instance.go,track.go,token.go— рантайм, который формализует этот ADR (тип token убран; event loop; жизненный цикл приведён).
9. Вне области — принадлежит будущим ADR¶
Этот ADR — ядро рантайма. Следующее надстраивается над ним и специфицируется в выделенных ADR (боковые ссылки, согласованные по иерархии); у каждого свой приёмочный гейт, и приземляется он со своим SRD + кодом:
| Концерн | Владелец | Статус |
|---|---|---|
Семантика join/merge — синхронизирующий join, несинхронизирующий merge, OR-join, Event-Based Gateway + причина Withdrawn; активация flow форка по типу шлюза |
ADR-005 v.4 Gateways & Joins | Accepted |
Доставка событий (EventHub → Instance), Terminate End Event, прерывающие boundary-события, узлы ожидания |
ADR-006 v.1 Events & Subscriptions | Accepted |
| In-memory модель освобождения на длинном ожидании (подписка → горутина заканчивается → повторное порождение) | ADR-007 In-Memory Long Waits | Draft |
Долговечная персистентность и восстановление после рестарта; контракт состояния на узел (исправляет мутацию разделяемого узла RegisterData, §6) |
Persistence & State ADR (будет написан) | — |
Состояния ошибок instance (Failing/Failed) и приостановка (Paused) |
будущие Error-Handling / Persistence ADR | — |
Политика «ADR на каждый адаптер». Каждый существенный адаптер расширения — персистентность (Repository), наблюдаемость (OpenTelemetry), обмен сообщениями, авторизация, диспетчеризация воркеров — специфицируется собственным ADR, а не целиком внутри ADR-002 или скелетного SRD (тривиальным умолчаниям вроде no-op или slog ADR не нужен). Скелет расширений (SRD-004) приземляет только минимальные, in-memory контракты-умолчания, нужные, чтобы исполнять сегодняшний BPMN на этой двухслойной модели; интерфейс Repository, как он определён в ADR-002 v.1, достаточен для этой цели миграции (только in-memory). Production-grade контракт каждого адаптера — для Repository: долговечная сериализация, версионирование/CAS, транзакции, history/inbox/subscriptions, пагинация — принадлежит его выделенному ADR (Persistence & State ADR выше).
История документа¶
| Версия | Дата | Автор | Изменение |
|---|---|---|---|
| v.6 | 2026-06-27 | Руслан Габитов | Фиксирует дозревание принципа единственного писателя (§2; §4.1 — track'и никогда не мутируют состояние Instance напрямую) и его §7-гейта свободы от гонок: владение теперь охватывает не только мутацию состояния instance, но и кросс-горутинные чтения — loop является единственным читателем разделяемого вида позиций токенов / join'ов, поэтому живой track больше не выставляет мутабельное состояние для чтения другой горутиной. Гонка чтения с чужой горутины, которую прежде латали по местам, тем самым устранена по построению; гейт теперь прогоняется в масштабе под -race (гейтится в CI, включая продлённые конкурентные стресс-прогоны). Концепция этого ядра не изменилась — реализующая это подсистема доставки событий принадлежит своему выделенному ADR (строка доставки событий в §9). На bump'е консолидированы устаревшие исходящие пины (§8 ADR-005 v.2→v.4; §9 статус ADR-005/006 →Accepted, проставлены пины). RU-twin синхронизирован до v.6 в этом change-set'е. |
| v.5 | 2026-06-11 | Руслан Габитов | §4.7 согласован с ADR-009 v.1: владение состоянием рантайма на каждый узел теперь решено (каждый instance клонирует шаблон процесса в свой собственный приватный граф узлов; состояние рантайма на каждый instance живёт на собственном узле этого instance; разделяемые определения шаблона остаются неизменяемыми), тогда как v.4 откладывала его целиком в Persistence ADR. Долговечная персистентность/сериализация/регидрация остаётся за будущим Persistence & State ADR. В остальном модель исполнения не изменилась. Синхронизация RU-twin отложена (пакетно). |
| v.4 | 2026-06-08 | Руслан Габитов | Зафиксирована политика «ADR на каждый адаптер» (§9): каждый существенный адаптер расширения получает свой ADR; скелет SRD-004 поставляет только минимальные in-memory контракты-умолчания, чтобы исполнять текущий BPMN на двухслойной модели, а production-grade контракты (в частности долговечный Repository — сериализация, версионирование, транзакции, history/inbox/subscriptions, пагинация) отложены в ADR на каждый адаптер. Минимального Repository из ADR-002 v.1 достаточно для цели in-memory миграции. Модель исполнения не изменилась. Синхронизация RU-twin отложена (пакетно, до устаканивания текущего раунда правок). |
| v.3 | 2026-06-07 | Руслан Габитов | Принято. Свёрнута трёхслойная модель до двух слоёв (Instance + track); token становится логической проекцией текущего шага track'а, не хранимым типом (убраны token.inst/trk, Instance.tokens[], дублирующее происхождение). Принята одна горутина event-loop для мутации состояния instance (без блокировок). Этот ADR ограничен ядром рантайма — join/merge (ADR-005), доставка событий и триггеры (ADR-006) и in-memory модель освобождения на длинном ожидании (ADR-007) перенесены в выделенные ADR (§9), чтобы документ был равен коду. Жизненный цикл instance приведён к Created → Active → Completed (+ Terminating → Terminated). Гейт §7 проверен и зелёный (свобода от гонок, без утечек, форк, проекция, завершение, каскад завершения) — включая два бага, вскрытых гейтом (гонка track.stopIt; emit, отбрасывавший evEnded при отмене). Персистентность/регидрация отложена в Persistence & State ADR. Pre-acceptance Draft-итерация свёрнута без построчных записей. |
| v.2 | 2026-05-29 | Руслан Габитов | Трёхслойная модель Instance/track/token (заменена v.3). |