ADR-009 — Per-instance граф узлов (runtime-состояние, принадлежащее узлу)¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.1 |
| Дата | 2026-06-10 |
| Владелец | Руслан Габитов |
| Уточняет | ADR-001 v.5 Execution Model |
EN-оригинал — канонический: ADR-009-per-instance-node-graph.md. Этот файл — его перевод (twin).
Область. Этот ADR решает, где живёт per-node runtime-состояние — вопрос, отложенный в ADR-001 §4.7. Это фундамент, на котором строится работа по синхронизирующим шлюзам (ADR-005), и он устраняет гонку данных на разделяемом узле. Он покрывает только владение runtime-состоянием в памяти; долговечная персистентность / регидрация этого состояния остаётся отдельным будущим концерном.
1. Контекст¶
ADR-001 определяет двухслойный рантайм: Instance владеет track'ами; узлы несут per-element поведение, которое исполняет track; и всё состояние жизненного цикла уровня instance мутируется единственной горутиной event-loop'а. ADR-001 §4.7 держит определения узлов неизменяемыми и разделяемыми между instance'ами и track'ами и явно откладывает per-node изменяемое runtime-состояние в будущий «Persistence & State ADR» — он так и не решает, где это состояние живёт.
Этот вопрос теперь на критическом пути. Нескольким элементам нужно per-instance, per-node runtime-состояние пока instance исполняется:
- синхронизирующий шлюз (ADR-005 v.2) должен накапливать, какие входящие flow доставили токен, на каждый join-узел, на каждый instance;
- узел timer должен держать свою позицию/следующее срабатывание;
- узел catch/receive должен держать свою подписку на message/signal.
Положить всё это некуда чисто. Разделяемый узел не может держать per-instance состояние: два конкурентных instance'а одного процесса — и даже два track'а одного instance'а, сходящиеся на одном узле — испортили бы друг друга. То же разделение уже порождает латентную гонку данных: runtime-загрузка данных мутирует разделяемое определение узла (случай data-path для End-event), что запрещает собственный инвариант неизменяемости ADR-001 §4.7.
По нашему стоящему принципу — более ранний документ поддерживает работу, а не заключает её в клетку; когда возникает лучшая модель, мы обновляем ранний документ — мы решаем отложенный вопрос здесь, уточняя ADR-001.
2. Решение¶
2.1 Каждый Instance владеет клонированным графом узлов¶
Определение процесса — неизменяемый шаблон. Когда Instance стартует, он клонирует шаблон в свой собственный приватный граф узлов, который живёт всё время жизни Instance. Instance'ы больше не разделяют объекты узлов; они разделяют только шаблон, из которого были клонированы.
flowchart TD
D["Process definition (immutable template) — node config + topology"]
D -->|clone at instance start| I1["Instance 1 — private node graph (each node owns its runtime state)"]
D -->|clone at instance start| I2["Instance 2 — private node graph (each node owns its runtime state)"]
L1["Instance 1 event-loop (sole writer of node state)"] --- I1
L2["Instance 2 event-loop (sole writer of node state)"] --- I2
2.2 Узел — это один stateful-объект на instance¶
Узел — это единственный объект, несущий и свою (неизменяемую) конфигурацию, и своё per-instance runtime-состояние. Его интерфейсные методы — исполнение, правило завершения синхронизирующего join'а, взвод timer'а, подписка — читают и пишут это состояние через ресивер. Нет отдельного объекта состояния и нет runtime-обёртки вокруг узла: один тип на вид узла, состояние и поведение вместе. (Почему не альтернативы: §4.)
2.3 Клонирование — поверхностное над конфигом, свежее над состоянием¶
Клонирование узла:
- разделяет неизменяемую конфигурацию по ссылке — определения событий, операции, условия, id, имена: ничего из этого не меняется в рантайме;
- выделяет свежее, обнулённое per-instance состояние и пустые коллекции flow.
Топология графа перестраивается повторной связкой клонированных узлов существующей в движке линковкой sequence-flow — без специальной хирургии графа. Стоимость старта на instance пропорциональна (узлы + flow); память остаётся низкой, потому что тяжёлый неизменяемый конфиг разделяется, и новыми являются только состояние и связка. Клонирование жадное (весь граф на старте instance) — просто и детерминированно; граф мал.
2.4 Event-loop остаётся единственным писателем состояния узла¶
Per-node runtime-состояние по-прежнему мутируется только единственной горутиной event-loop'а Instance (ADR-001). Track'и никогда не мутируют состояние узла напрямую — они сообщают через события, а loop применяет их к целевому узлу. Это держит мутацию узла без блокировок и сериализованной, и это ровно то, что делает per-instance узлы безопасными, когда несколько track'ов сходятся на одном узле (синхронизирующий join — это cross-track рандеву, разрешаемое в loop'е).
2.5 Определение vs. runtime-состояние — граница¶
Каждый вид узла классифицирует свои поля: конфигурация (неизменяемая, разделяемая по ссылке из шаблона) vs. runtime-состояние (свежее на instance). Только runtime-состояние является per-instance; геттеры над конфигурацией читают разделяемые данные шаблона. Эта граница — контракт, которому следует автор узла при добавлении вида узла (и то, что клон копирует vs. выделяет).
3. Последствия¶
- Синхронизирующие шлюзы (ADR-005), timer'ы и корреляция message получают естественный дом для per-instance состояния на самом узле — например, join-узел держит свой собственный учёт прибытий.
- Гонка данных на разделяемом узле устранена. Runtime-мутация (загрузка данных, data-path, состояние timer'а/подписки) теперь нацелена на per-instance узлы, поэтому конкурентные instance'ы и track'и не могут испортить разделяемое определение. Это закрывает опасность, которую отметил ADR-001 §4.7 и которую ADR-005 пришлось обходить — race-detector должен быть чист для конкурентных instance'ов одного процесса.
- Стоимость: каждый Instance выделяет свой граф узлов на старте (клонирование узлов + повторная связка flow) — ограниченная, конфиг разделяется.
- Правило сопровождения: каждый вид узла реализует поверхностное клонирование и декларирует своё разделение config/state; новый вид узла обязан сделать это, иначе он молча снова начнёт разделять состояние.
- Уточняет ADR-001 §4.7: владение runtime-состоянием теперь решено (per-instance, принадлежащее узлу). Долговечная персистентность — сериализация этого состояния и его регидрация через рестарт — остаётся отдельным будущим Persistence & State ADR; этот ADR — только владение в памяти. ADR-001 §4.7 обновляется соответственно, когда это лендится.
4. Рассмотренные альтернативы¶
- Разделяемые неизменяемые узлы + отдельный per-instance объект состояния (по ключу node id). Отклонено: интерфейсные методы узла не могут чисто дотянуться до состояния — вы либо передаёте состояние внутрь, либо держите обратные указатели, размазывая state-plumbing по каждой интерфейсной сигнатуре.
- Разделяемое определение + per-instance runtime-обёртка, встраивающая определение. Отклонено: два типа на вид узла и постоянный раскол «метод определения или runtime-метод?»; расширение интерфейса тогда рискует затронуть оба на каждый вид — прямой налог на ключевую расширяемость движка (добавить-узел / добавить-поведение).
- Generic-структура, держимая Instance'ом (например,
map[node][flow][]trackв loop'е). Отклонено: это работает, но разводит состояние и узел и неудобно для чтения и расширения — вложенные мапы там, где место типизированному полю на узле. - Оставить узлы разделяемыми, запретить per-node состояние. Отклонено: синхронизирующие join'ы, timer'ы и корреляция фундаментально нуждаются в per-instance состоянии; это лишь перемещает проблему и оставляет гонку данных на месте.
5. Ссылки¶
- ADR-001 v.5 Execution Model — двухслойный рантайм, единственный писатель event-loop'а и §4.7 (инвариант неизменяемости узла + отложенное per-node-состояние, которое решает этот ADR; обновляется, когда это лендится).
- Объектная модель BPMN 2.0 — конфигурация узла (события, активности, шлюзы и их определения) является неизменяемым шаблоном; per-instance runtime-состояние — собственный концерн движка.
- Persistence & State ADR (будущий) — долговечная сериализация / регидрация per-instance состояния узла, которое устанавливает этот ADR.
6. Открытые вопросы¶
- Ничего блокирующего. Точное разделение config/state по видам и сигнатуры клонирования — концерн реализации для лендящего SRD, а не этого решения.
История документа¶
| Версия | Дата | Автор | Изменение |
|---|---|---|---|
| v.1 | 2026-06-10 | Руслан Габитов | Первая. Решает владение per-node runtime-состоянием (отложенное в ADR-001 §4.7): каждый Instance клонирует неизменяемый шаблон процесса в свой граф узлов; узел — это один per-instance stateful-объект, держащий конфиг (разделяемый по ссылке) + свежее runtime-состояние, с поведением и состоянием на одном типе (нет отдельного объекта состояния, нет обёртки); клонирование — поверхностное-над-конфигом + свежее-над-состоянием с топологией, перевязанной через существующую линковку sequence-flow; event-loop остаётся единственным писателем (без блокировок). Устраняет гонку данных на разделяемом узле. Уточняет ADR-001 v.5 §4.7 (владение runtime-состоянием решено здесь; долговечная персистентность остаётся будущим Persistence & State ADR). Отклонено: отдельный объект состояния, встраивающая обёртка (удвоение типов), generic-мапы у Instance, без-per-node-состояния. |