ADR-023 — Модель выполнения Sub-Process и Call Activity (вложенные области)¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.5 |
| Дата | 2026-08-28 |
| Владелец | Руслан Габитов |
| Уточняет | ADR-001 v.6, ADR-010 v.2 §2.2, ADR-018 v.1 §2.2/§2.6, ADR-006 v.6 §2.6/§2.7, ADR-019 v.1, SAD-001 v.1.3 §15.3 |
EN-оригинал — канонический: ADR-023-sub-process-and-call-activity.md. Этот файл — его перевод (twin). При расхождении приоритет у английского текста.
Этот ADR решает, как процесс gobpm композируется, на одном понятии: область выполнения (execution scope) — совместный контекст токенов, данных и событий из §10.5.7 — как дерево. Встроенный Sub-Process — это узел-контейнер, открывающий дочернюю область внутри того же экземпляра, с валидированными формами инстанциации, завершением по осушению (drain), отменой области как единицей прерывания и обходом цепочки областей для Error. Event Sub-Process — обработчик, взведённый на время жизни этой области. Call Activity — граница переиспользования, которую проводит сам стандарт, и потому единственное место, где появляется дочерний экземпляр: callable, разрешаемый через версионируемый реестр во время вызова, со стандартным прямым отображением I/O и каскадом отмены.
1. Контекст и проблема¶
Процесс, который движок не умеет вкладывать, — это процесс, неспособный выразить переиспользование (фрагмент, общий для нескольких определений), структуру (отменяемая, компенсируемая единица работы) и контейнеры-обработчики событий из §13.5.4. Это не три фичи, а одно недостающее понятие, и форма этого понятия решает сразу несколько вещей: что такое контекст токена, где данные живут и умирают, какой обработчик ловит брошенную ошибку и что значит «эта активность закончилась», когда активность содержит граф.
Риск отвечать на них по отдельности — движок «на каждую конструкцию своё»: одно правило завершения sub-process'а, другое — времени жизни обработчика, третье — что именно терминирует Terminate. Этот ADR отвечает один раз, и ограничение, которое он обязан соблюсти, — это единственный писатель цикла gobpm (ADR-001 v.6): композиция должна расширять учёт цикла, а не добавлять второй домен синхронизации.
2. Решение¶
2.1 Одна концепция: область выполнения¶
Область — это контекст выполнения из §10.5.7, совместный набор:
- токены: граф flow-узлов, в котором движутся токены области;
- данные: переменные и data objects, видимые по обходу контейнеров вверх (Property Sub-Process'а доступно этому Sub-Process'у и его непосредственным детям; данные родителя видны из ребёнка, но никогда наоборот);
- события: обработчики, взведённые пока область активна, — boundary-события на её композитном хосте и event sub-process'ы, объявленные в ней (§2.10).
Области образуют дерево с корнем в экземпляре процесса. gobpm идентифицирует
область её путём: корень экземпляра — /, встроенный sub-process orders
открывает /orders, вложенный retry — /orders/retry, — дословно
переиспользуя адресацию контейнерных областей плоскости данных, так что дерево
данных и дерево выполнения — одно и то же дерево.
Встроенный sub-process выполняется внутри родительского экземпляра. Один экземпляр, один цикл событий, один единственный писатель: вложенные токены — это обычные треки, которые дополнительно несут свой путь области, а реестры цикла получают осведомлённость об областях вместо дублирования на каждую область. Это тот же приём «расширяй учёт, а не модель конкурентности», что и в channel-based-обработке событий, и именно он держит композицию вне дизайна блокировок. Дочерний экземпляр существует только за границей Call Activity (§2.7) — там, где линию переиспользования проводит сам стандарт.
flowchart TD
subgraph inst["instance scope /"]
s((start)) --> t1[task A]
t1 --> sp
subgraph sp["sub-process scope /orders"]
s2((start)) --> t2[approve]
t2 --> e2((end))
end
sp --> t3[task B]
t3 --> e((end))
end
2.2 Узел встроенного Sub-Process — контейнер, который является узлом¶
Sub-Process — это и то и другое: flow-узел в графе родителя (у него есть входящие и исходящие sequence flow, boundary-события, жизненный цикл активности) — и контейнер собственного внутреннего графа.
Вложенность строгая (Table 7.2 p.29; §7.6.1 p.40): «Sequence Flows не могут пересекать границу Sub-Process» — внутренний узел соединяется только с внутренними узлами, а композит соединяется с графом родителя лишь через рёбра собственного узла и boundary-события.
Решение движка — самопротиворечие §13.3.4 разрешено в пользу §7.6.1. §13.3.4 (p.430) содержит абзац, разрешающий инстанциировать sub-process без входящих потоков через «Start Events, являющиеся целью Sequence Flows извне Sub-Process». Это прямо противоречит правилам соединения выше и явному примечанию Table 7.2, является рудиментом BPMN 1.x и не реализовано ни одним референсным движком. gobpm отвергает пересекающие границу потоки безусловно; пункт не поддерживается.
2.3 Инстанциация — детерминированные, валидированные формы¶
Проверено дословно (§13.3.4 p.430): «Sub-Process инстанциируется, когда в него приходит токен по Sequence Flow. У Sub-Process есть либо единственное пустое Start Event, которое получает токен при инстанциации, либо нет Start Event, но есть Activities и Gateways без входящих Sequence Flows. В последнем случае все такие Activities и Gateways получают токен. Sub-Process НЕ ДОЛЖЕН иметь непустых Start Events». Table 10.85 (p.241) обосновывает почему: «поток Процесса (токен) из родительского Процесса и есть триггер Sub-Process'а».
gobpm поддерживает обе нормативные формы и отвергает всё остальное на валидации процесса — до того, как появился хоть один экземпляр, — так что некорректный композит является ошибкой регистрации, а не сюрпризом времени выполнения:
| Форма | Поведение |
|---|---|
| Ровно одно None Start Event | Старт получает входной токен; внутренний поток идёт от него. |
| Нет Start Event | Каждая внутренняя активность и шлюз без входящих потоков получают токен (параллельный веер стартов). |
| Триггерный Start Event внутри | Отвергается — Message/Timer/Signal/Conditional-старты принадлежат event sub-process'ам (§2.10) или процессам верхнего уровня. |
| None-старт вперемешку с другими узлами без потоков | Отвергается — спецификация формулирует формы как взаимоисключающую альтернативу; смешанная форма не определена, а тихие полустарты — тот класс некорректного поведения, на котором gobpm падает быстро. |
| Более одного None-старта | Отвергается — «единственное пустое Start Event». |
Общие правила активностей не тронуты: несколько входящих потоков в композит —
это неявное исключающее слияние, каждый пришедший токен — независимая
инстанциация (§13.3.1). startQuantity/completionQuantity ≠ 1 —
намеренная не-цель (SAD-001 v.1.3 §4
N8); рантайм соблюдает значение по умолчанию 1.
2.4 Жизненный цикл области — open, seed, drain, close¶
Когда выполнение входит в узел Sub-Process:
- Открыть дочернюю область: её контейнер данных открывается под путём родителя, а её обработчики взводятся — boundary-события композита на хосте и event sub-process'ы, объявленные внутри него (§2.10).
- Засеять внутренние токены по валидированной форме (§2.3) — внутренние треки, несущие путь дочерней области.
- Выполнение композита ожидает своё тело. Ожидание внутриэкземплярное: для встроенного sub-process'а никакого дочернего экземпляра нет (§2.1), внутренние треки — братья в том же цикле, и ожидание заканчивает собственный учёт осушения цикла, а не внешнее завершение. (Парковка как таковая — трек, приостановленный в ожидании внешнего события, — это форма Call Activity, §2.7. Чем и как приводится в движение выполнение композита — решение ADR-025 v.5.1 §2.12/§2.13, а не этого ADR.)
- Завершение по осушению (§13.3.4): область завершается, когда внутри неё не осталось токенов — каждый внутренний трек закончился и ни одна внутренняя активность больше не активна. Потрековый учёт цикла расширяется на область.
- Закрыть область: её контейнер данных закрывается, и внутренние переменные утилизируются вместе с ним (жизненный цикл DataObject привязан к его контейнеру, §10.5.7); её обработчики снимаются; композит завершается и выбирает исходящие потоки по стандартным правилам активности, включая условные потоки и default.
Внутренние End Events сохраняют своё поведение — Message-конец отправляет, Signal-конец вещает — и завершают собственный трек, питая правило осушения. Error End Event и Terminate End Event получают семантику, осведомлённую об областях (§2.5, §2.6).
2.5 Прерывание — область отменяется как единое целое¶
Всё, что ADR-018 v.1 решил для одиночной активности, распространяется на композит заменой «отменить трек» на «отменить область»: остановить каждый трек, чей путь лежит внутри области, той же кооперативной отменой, закрыть область и продолжить согласно прерывающей конструкции.
- Boundary-события на композите (прерывающие): срабатывание отменяет дочернюю область и направляет токен на exception flow границы; окно взведения/снятия — это окно выполнения хоста, без изменений. Непрерывающие границы ветвятся параллельно, тоже без изменений. Применяется полный набор триггеров границы — Message, Timer, Signal и Conditional по их существующим моделям, Error по §2.6.
- Областной Terminate (§13.5.6, проверено): Terminate End Event «терминирует свою объемлющую область — для sub-process'а только затронутый экземпляр; области более высокого уровня НЕ затрагиваются». Достижение Terminate внутри sub-process'а отбрасывает оставшиеся токены только этой области и завершает композит аварийно, но локально; родитель продолжает. Terminate верхнего уровня сохраняет семантику всего экземпляра, потому что экземпляр — это просто корневая область. Terminate не запускает компенсацию (ADR-006 v.6 §2.2; opt-in-переопределение — ADR-026 v.1 §2.8, выключено по умолчанию).
- Терминирование или остановка экземпляра отменяет корневую область, что каскадом расходится по дереву на каждую вложенную.
2.6 Распространение Error — цепочка областей¶
Ошибка находит свой обработчик обходом наружу, и обход один и тот же для любого уровня области:
- Активность падает с
BpmnError→ сопоставить Error-границу на этой активности. - Нет совпадения → идти наружу: на каждой объемлющей области сопоставить Error-границу на композитном хосте этой области и event sub-process с Error-стартом, объявленный в этой области (§2.10), — ближайший объемлющий ловец, §10.5.1/§10.5.7. Совпадение отменяет эту область (Error всегда прерывающий) и направляет её exception flow.
- Нет совпадения нигде по цепочке → отказ экземпляра, решение движка о неперехваченной ошибке, зафиксированное в ADR-006 v.6 §2.6.
- Error End Event внутри sub-process'а бросает на границе своей области: обход начинается с объемлющего композита, поэтому error-конец во вложенной области ловится родителем, и только неперехваченный роняет экземпляр.
Escalation следует той же цепочке с некритичной семантикой.
2.7 Call Activity — дочерний экземпляр через реестр¶
Call Activity — это граница переиспользования в стандарте (§13.3.4): она
вызывает CallableElement — для gobpm отдельно зарегистрированный процесс.
Композиция идёт по ссылке, а не по вложению, поэтому единица выполнения —
дочерний экземпляр, а не вложенная область.
- Разрешение и привязка версии.
calledElement— это ссылка на callable: ключ, опционально квалифицированный пространством имён документа definitions, в котором callable был объявлен. Неквалифицированная ссылка называет ключ реестра напрямую (ADR-019 v.1). Привязка по умолчанию — latest-at-launch, семантика реестра «просто запусти текущий», а закреплённая версия — явная опция на Call Activity. Разрешение происходит во время вызова, поэтому callable может быть зарегистрирован позже или переверсионирован; отсутствующий ключ или версия роняют активность вызывающего классифицированной ошибкой, входящей в цепочку §2.6 как техническая неисправность. - Разрешение callable — шов на стороне host'а; решение движка. Стандарт
типизирует
calledElementкак простойStringи оставляет инструменту то, как ссылка находит свойCallableElement; ссылка в другой документ осмысленна только через<import>этого документа. Поэтому движок не владеет никакой конвенцией именования. Резолвер callable — контракт host'а, поставляемый один раз на движок, — превращает ссылку на callable в обслуживаемый реестром ключ и вызывается во время вызова для каждого вызова, вне всех блокировок движка, потому что это код host'а. Резолвер по умолчанию сохраняет неквалифицированный случай точным (ссылка и есть ключ) и отвечает на квалифицированную ссылку классифицированной ошибкой, называющей пространство имён: host, который никогда не импортирует между документами, не настраивает ничего, а тот, который импортирует, поставляет отображение вместо того, чтобы движок его угадывал. - Global task — это callable, и реестр обслуживает его как процесс; решение
движка. Семейство
GlobalTaskв стандарте — этоCallableElementбез собственного потока, а §13.3.4 придаёт вызову семантику вызываемого Процесса. Движок реализует global task как процесс, телом которого является эта одна задача: None-старт, задача, None-конец, — который host регистрирует под id global task'а, как любой другой процесс, аioSpecificationcallable становится объявленным контрактом процесса (ADR-040 v.2). Переиспользование остаётся по ссылке: одна регистрация, любое число вызывающих, а перерегистрация чеканит версию, как у всякого процесса. На пути вызова ничто не отличает вызванный global task от вызванного процесса — а это ровно то, чем §13.3.4 называет вызов. - Семантика вызова (§13.3.4, проверено): вызываемый процесс инстанциируется своим None Start Event; его триггерные Start Events — законные для глобального процесса — игнорируются на пути вызова («эти непустые Start Events являются альтернативой пустому Start Event и потому игнорируются, когда Процесс вызывается»). У вызванного экземпляра та же семантика инстанциации и терминирования, что и у sub-process'а.
- Вызывающий ждёт внешнюю работу. Call Activity — настоящий узел ожидания в родителе: дочерний экземпляр крутит собственный цикл, а его терминальное состояние возвращается в цикл вызывающего и возобновляет ждущий трек. Completed → выходы связываются и вызывающий продолжает; Terminated или Failed → неисправность входит в цепочку §2.6 вызывающего на узле Call Activity и ловится Error-границей на нём.
- I/O — прямое отображение стандарта (проверено): DataInputs/DataOutputs Call Activity отображаются на InputOutputSpecification callable без явных data associations — прямое связывание по имени. Входы связываются в корневую область ребёнка при запуске; выходы связываются обратно в область вызывающего при завершении. Плоскость данных ребёнка изолирована: обход вверх не пересекает границу вызова, потому что вызываемый процесс обязан выполняться одинаково, как бы до него ни дошли, и эта же изоляция является гарантией приватности — пересекают границу только объявленные входы. Сторона вызываемого в этом отображении — собственные объявленные входы и выходы процесса, что связывается при его запуске, что читается при его завершении и проверка по именам параметров вызывающего против разрешённого callable при запуске — это ADR-040 v.2.
- Каскад отмены — решение движка (стандарт молчит о терминировании по инициативе вызывающего): отмена Call Activity вызывающего — прерывающая граница на ней, областной Terminate её области или терминирование экземпляра — каскадирует Terminate в дочерний экземпляр. Вызов «выстрелил и забыл» вне области рассмотрения: ни одна конструкция BPMN его не выражает.
- Связность в наблюдаемости. Факты дочернего экземпляра несут родительскую связь — id экземпляра вызывающего и id узла Call Activity, — так что трасса сшивается через границу.
- Контракт перезапуска — вызов переживает рестарт целиком (стандарт молчит о рестартах движка; решение движка, управляемое ADR-033 v.5 §2.10): дочерний экземпляр долговечен сам по себе, чекпоинтится под тем же репозиторием и дисциплиной, что и любой экземпляр, и несёт родительскую связь, тогда как чекпоинт вызывающего записывает вызов в полёте — id ожидаемого дочернего экземпляра и узел вызова. Восстановление поднимает оба конца и связывает их заново: ждущий трек вызывающего возобновляется на восстановленном ребёнке, терминальное состояние ребёнка входит в восстановленного вызывающего ровно так же, как вошло бы в резидентного, и каскад отмены переживает рестарт. Восстановленный вызывающий, у которого нет записи об ожидаемом ребёнке, громко проваливает своё восстановление — вызов является записанным состоянием, поэтому тихий перезапуск ребёнка спроектирован прочь; и наоборот, восстановленный ребёнок, чья родительская запись исчезла, падает громко, а не работает сиротой.
2.8 Композиты, которые едут на этой модели областей¶
Область намеренно общая, и несколько конструкций являются её вариантами, а не новыми моделями выполнения. Каждая решается своим ADR и берёт жизненный цикл данных области, завершение по осушению и отмену без изменений:
- Transaction — вариант sub-process'а, чьи Cancel-конец и Cancel-граница едут на отмене области плюс компенсации (ADR-028 v.2).
- Ad-Hoc — контейнер, чьё внутреннее разрешение к выполнению управляется выбором, а не потоком: переиспользует область и заменяет только правило засева токенов (ADR-035 v.1).
- Итерация — Standard Loop и Multi-Instance над активностью, в том числе композитной. Какие активности получают дочернюю область на итерацию, а какие итерируются на месте — решение ADR-025 v.5.1, а не этого ADR.
- Компенсация — реестр завершений, который несёт дерево областей, и обработчики, которые он вызывает (ADR-026 v.1).
2.9 Рекурсия и глубина¶
Глубина вложенности не ограничена by design: области — дерево, а пути композируются. Call Activity может вызвать собственный процесс — рекурсия является законной композицией, а разрешение идёт по ключу реестра во время вызова, — поэтому статическая проверка циклов ни требуется стандартом, ни разрешима между версиями. Разбежавшаяся рекурсия — ошибка моделировщика, ответ на которую даёт эксплуатационный предохранитель глубины (§6), а не запрет на уровне модели.
2.10 Event Sub-Process — обработчик, взведённый областью¶
Event Sub-Process (§13.5.4) — это SubProcess с пометкой triggeredByEvent:
фрагмент-обработчик, живущий внутри области и взводимый событием, а не
достигаемый sequence flow. Это областной аналог boundary-события: там, где
граница охраняет окно одной активности, event sub-process охраняет окно целой
области.
Модель. Event sub-process переиспользует контейнер §2.2 и вложенную область §2.4 и добавляет ровно то, чего требует и что запрещает стандарт:
- Единственное триггерное Start Event (§10.5.2 p.241: «Event Sub-Process
ДОЛЖЕН иметь единственное Start Event») одного вида — Message, Timer, Signal,
Error, Conditional или Escalation; флаг
isInterruptingстарта выбирает вариант. - Самодостаточен (§13.5.4): никаких sequence flow в граф родителя или из него, достигается только срабатыванием своего старта. Поскольку его старт триггерный, он никогда не является входным узлом: инстанциация §2.3 засевает None-старт или узлы без потоков и пропускает event sub-process, регистрируя его вместо этого как обработчик области.
- Никаких boundary-событий на нём (§13.5.4).
- Выполняется в контексте данных родителя (§13.5.4): его внутренние узлы читают объемлющую область обходом вверх §2.4, как любой внутренний узел.
Взведение — паттерн boundary-watch на гранулярности области. Когда область открывается (§2.4 или корень экземпляра при старте), каждый event sub-process, объявленный непосредственно в ней, взводится, регистрируя триггер своего старта той же пер-видовой машинерией, что использует boundary-событие:
| Триггер | Механизм взведения |
|---|---|
| Message | ожидатель в хабе по ключу сообщения, доставляется как событие цикла |
| Signal | ожидатель в хабе на имя сигнала (безопасно к вещанию) |
| Timer | подсистема таймеров, срабатывающая в цикл |
| Conditional | локальная для цикла условная подписка на фронте false→true (ADR-006 v.6 §2.7) — сюда приземляется условный старт, и он законно читает объемлющую область |
| Error | обход цепочки областей §2.6 в точке броска — никакого взведённого ожидателя; обход находит ближайшую объемлющую область, чей обработчик ловит код |
Обработчик взведён на время жизни своей области — снимается, когда область осушается (§2.4), отменяется (§2.5) или, для прерывающего обработчика, как только любой прерывающий обработчик в области сработал.
Прерывающий — отменить область, выполнить обработчик в ней. По прерывающему триггеру:
- Братские треки объемлющей области отменяются — отмена области §2.5, применённая к собственной области обработчика. Плоскость данных области остаётся открытой; обработчик выполняется в ней.
- Жизненный цикл родителя отражает триггер (§13.5.4): Error-старт переводит родителя в Failing, а не-error прерывающий старт — в Terminating. Движок не держит отдельного состояния токена Failing/Terminating — сообразно выбору §2.1 не добавлять состояния жизненного цикла активности, в которых цикл не нуждается, — и реализует различие тем, какой путь отмены выполняется (обход Error §2.6 против простой отмены области), выводя его в наблюдаемость.
- Внутренний поток обработчика засевается из его триггерного старта, причём старт считается сработавшим, а его полезная нагрузка связывается способом «рождённого события» §2.4, в очищенную область; обработчик выполняется до своего End в контексте данных области.
- Когда обработчик осушается, область завершается и композит возобновляется, следуя исходу обработчика.
Общий прерывающий бюджет и absorb против re-throw. Для данного Event
Declaration в области может сработать только один прерывающий обработчик — и
этот бюджет общий между event sub-process'ом и любым boundary-событием на том же
хосте области (§10.5.6 p.278). Локус поэтому — один регистр прерывающего
взведения на область, ключёванный по Event Declaration, к которому обращаются и
boundary-watch, и взведение event sub-process'а; первое прерывающее срабатывание
переводит область в interrupted и снимает остальные. Когда область несёт и
встроенный event sub-process, и границу на тот же EventDefinition:
- обработчик завершается без повторного броска → он поглощает событие: граница не срабатывает, и родитель возобновляется на своём нормальном потоке;
- обработчик бросает событие повторно — Throw того же вида на его End → граница срабатывает после обработчика: встроенный обработчик является декоратором, а не терминальным.
Это даёт моделировщику явный контроль «терминальный против декоратора».
Непрерывающий. Непрерывающий event sub-process (isInterrupting=false, любой
триггер кроме Error — ошибки всегда прерывают, §10.5.6) выполняется
параллельно с родителем: триггер потребляется, экземпляр обработчика
порождается новым треком в свежей дочерней области под родителем, и родитель
продолжает работать. Несколько могут выполняться одновременно (§10.5.6:
неограниченно, порядок недетерминирован); каждый — своя область, и родитель не
завершается по осушению, пока не осушится каждый порождённый обработчик, что учёт
§2.4 уже покрывает, считая каждый трек под путём.
3. Grounding по стандарту¶
| Утверждение | Источник |
|---|---|
| Инстанциация токеном родителя; единственный None-старт XOR засев узлов без потоков; непустые старты запрещены | §13.3.4 p.430 (проверено дословно) |
| «Поток Процесса (токен) из родительского Процесса и есть триггер Sub-Process'а»; None — единственный тип старта sub-process'а | §10.5.2 p.241 + Table 10.85 |
| Sequence flow не могут пересекать границу sub-process'а | Table 7.2 p.29; §7.6.1 p.40 (абзац §13.3.4 о внешнем старте отвергнут как самопротиворечивый — решение движка §2.2) |
| Завершение = внутри не осталось токенов, ни одна внутренняя активность не активна | §13.3.4 p.430 (sub-processes.md) |
| Область = контекст данных/событий/бесед; видимость property родитель→дети; жизненный цикл DataObject привязан к контейнеру | §10.5.7 (data.md) |
| Областной Terminate — только затронутый (под)экземпляр; вышестоящие области не затронуты | §13.5.6 p.443 (event-handling.md) |
| Error и Escalation распространяются к ближайшему объемлющему ловцу; Error критичен | §10.5.1 / §10.5.7 (event-handling.md) |
| Call Activity вызывает CallableElement; та же семантика инстанциации/терминирования, что у sub-process'а; триггерные старты вызываемого процесса игнорируются на пути вызова | §13.3.4 p.430-431 (проверено дословно) |
calledElement — простой String, 0..1; стандарт не фиксирует для него конвенции именования |
собственная таблица свойств CallActivity (elements/activities.md) |
Ссылка в другой документ definitions объявляется через Import (importType, location, namespace) |
таблица элемента Import (elements/foundation.md) |
GlobalTask и его четыре подтипа — это CallableElementы (GlobalTask → CallableElement → RootElement), несущие name, ioSpecification и resources, и не имеющие собственного потока |
таблицы семейства GlobalTask (elements/activities.md) |
| Только Tasks и CallableElements (Processes, GlobalTasks) МОГУТ объявлять DataInputs/DataOutputs | §10.4.1 p.210 (data.md, правила вложенности) |
| I/O Call Activity отображается на callable без явных data associations | семантика данных (data.md, строка CallActivity) |
| Триггеры границы на композитах; Error всегда прерывающий | §10.5.4 / §10.5.6 (event-handling.md) |
Event sub-process: triggeredByEvent; единственный триггерный старт; самодостаточность; никаких boundary-событий; контекст данных родителя; Error→Failing, не-error прерывающий→Terminating |
§13.5.4 p.436-439; §10.5.2 p.241 |
| Один прерывающий обработчик на Event Declaration, общий между event sub-process'ом и границей; непрерывающих неограниченно; Error только прерывающий | §10.5.6 p.278 (event-handling.md) |
| Встроенный обработчик поглощает (нет повторного броска → граница подавлена) против повторного броска (→ граница срабатывает после) | §10.5.6 p.278 (event-handling.md) |
| Неявное исключающее слияние на нескольких входящих потоках | §13.3.1 p.427 |
Строки, ссылающиеся на таблицу элемента, цитируют файл вендоренного извлечения: структурная метамодель переписана из bpmn-moddle и не несёт собственного § OMG.
Умолчания стандарта, разрешённые как решения движка: абзац о старте, пересекающем границу (§2.2); каскад отмены от вызывающего в вызванный экземпляр и шов разрешения callable (§2.7); отказ экземпляра при неразрешённой ошибке (унаследовано из ADR-006 v.6 §2.6); жизненный цикл родителя Failing/Terminating, реализованный путём отмены плюс наблюдаемостью, а не отдельным состоянием токена (§2.10); и параллельные непрерывающие обработчики, выполняющиеся в недетерминированном порядке треков цикла, что соблюдено отсутствием каких-либо гарантий порядка.
4. Рассмотренные альтернативы¶
| Альтернатива | Почему отвергнута |
|---|---|
| Дочерний экземпляр на каждый встроенный sub-process (единообразно с Call Activity) | Встроенный sub-process разделяет контекст родителя (видимость §10.5.7; обработчики читают данные объемлющей области), поэтому дочернему экземпляру потребовался бы межэкземплярный мост данных, который обход вверх даёт бесплатно. Это множит циклы и событийную обвязку ради нулевого выигрыша в изоляции и превращает связность завершения и отмены в межэкземплярный протокол вместо учёта внутри цикла. Граница экземпляра осмысленна — стандарт ставит её на линии переиспользования. |
| Инлайнинг графа для Call Activity (копировать вызываемый граф в snapshot вызывающего при регистрации) | Ломает контракт переиспользования: привязка замерзает на регистрации там, где ADR-019 даёт latest-at-launch; рекурсия становится невозможной из-за бесконечного разворачивания; собственная наблюдаемость и версионная идентичность вызываемого процесса исчезают; изоляция вызывающего и вызываемого теряется. |
| Уплощение встроенного sub-process'а в родительский граф с префиксацией имён узлов и без области времени выполнения | Теряет ровно то, ради чего композит существует: завершение по осушению, отмену области, областной Terminate, точки сопоставления цепочки ошибок, жизненный цикл данных на область — и каждое из этого потребовало бы поузловых частных случаев, которые концепция области даёт один раз. |
| Глобальный для движка реестр областей (области как first-class объекты движка вне экземпляров) | Ничто не пересекает границу экземпляра, кроме протокола Call Activity; вынос областей наружу заново ввёл бы блокировки разделяемого состояния, которые убрала channel-based-модель событий. |
| Event sub-process'ы как реестр обработчиков верхнего уровня (глобальные ловцы) | Ломает локальность области, которую предписывает стандарт (обработчик ловит только внутри своей области), и дублирует цепочку §2.6. Event sub-process и есть область, поэтому параллельный областям рантайм переизобрёл бы §2.2–§2.5. |
| Event sub-process'ы, смоделированные как N boundary-событий на хосте области | Граница охраняет занятое окно одной активности; event sub-process охраняет открытое окно области (другое время жизни), ловит откуда угодно изнутри и выполняется в контексте данных области. Общий прерывающий бюджет — единственное пересечение, и он смоделирован общим регистром на область, а не схлопыванием двух конструкций. |
5. Последствия¶
- Движок получает композицию: переиспользование через Call Activity поверх версионируемого реестра, структуру через отменяемые единицы и контейнер, вариантами которого являются конструкции transaction, ad-hoc и обработчиков.
- Цикл остаётся единственным писателем. Вложенное выполнение добавляет осведомлённость об областях существующим реестрам — трек знает свой путь области, завершение и отмена учитываются по поддереву, — а не второй домен синхронизации.
- Плоскости данных не нужно новое понятие: дерево контейнерных областей, уже построенное ADR-010 v.2 §2.2, задействует свои дочерние области, а видимость и утилизация приходят из существующих обхода вверх и закрытия.
- Механизм границ не меняется: граница-на-композите — тот же интерфейс с композитным хостом и отменой области за ним, чего и ожидал ADR-018 v.1 §2.6.
- Error End Event перестаёт всегда ронять экземпляр — он становится ловимым объемлющими областями, и только неперехваченная ошибка доходит до экземпляра.
- Terminate End Event внутри sub-process'а означает «терминировать область», а не «терминировать экземпляр». Семантически это новое, а не слом, поскольку Terminate раньше вообще не мог находиться внутри композита.
- Учёт областей концентрируется в цикле — счётчики активных на область, отмена поддерева, детекция осушения. Держать это табличным учётом под единственным писателем — то, что делает его детерминированно тестируемым.
6. Рекомендации по enterprise-готовности¶
- Наблюдаемость. Жизненный цикл области должен быть first-class в потоке фактов — область открыта, завершена, отменена, с путём области и идентичностью композитного узла, — потому что оператор рассуждает в единицах sub-process'ов, а не сырых треков. (Сама граница вызова уже сделана: она несёт родительскую связь и разрешённую пару (ключ, версия), которую решил резолвер, §2.7, так что аудит показывает, какая регистрация действительно выполнялась, а не какую назвал файл. Там, где этот ключ ещё непознаваем — переприкреплённый ребёнок до того, как стал резидентным, — атрибут отсутствует, а не приближается ссылкой, которая назвала бы другую регистрацию; ADR-022 v.3 §2.5.)
- Эксплуатационный предохранитель глубины. Выставить опцию движка для максимальной глубины дерева областей и цепочки вызовов (щедрую по умолчанию), роняющую экземпляр классифицированной ошибкой с именованием цепочки, — что превращает разбежавшуюся рекурсию из исчерпания ресурсов в диагностируемую неисправность.
- Дисциплина закрепления версий. Latest-at-launch — правильное умолчание, но продуктивные вызывающие должны иметь возможность закрепиться (§2.7), а эволюция callable требует документированного пути устаревания: зарегистрировать новую версию, мигрировать вызывающих, вывести старую.
- Контрактное тестирование. Вызываемый процесс — это интерфейс: его объявленный I/O есть контракт, против которого связывается вызывающий (ADR-040 v.2 §2.4 валидирует пару при запуске, поскольку latest-at-launch означает, что пара известна только тогда).
7. Область охвата и отложенное¶
В охвате: область выполнения как дерево (§2.1); встроенный Sub-Process как узел-контейнер с валидированными формами инстанциации, завершением по осушению и жизненным циклом области (§2.2–§2.4); отмена области как единица прерывания, включая областной Terminate (§2.5); цепочка областей для Error (§2.6); Call Activity как дочерний экземпляр с разрешением callable, прямым I/O, каскадом отмены и контрактом перезапуска (§2.7); неограниченная вложенность (§2.9); и Event Sub-Process как обработчик, взведённый областью, с его общим прерывающим бюджетом и приоритетом absorb-против-re-throw (§2.10).
Решено в другом месте — указатели, а не отложенное (§2.8): семантика транзакции ADR-028 v.2; ad-hoc-выбор ADR-035 v.1; итерация над активностью и то, что приводит в движение выполнение композита, ADR-025 v.5.1 §2.12/§2.13; компенсация ADR-026 v.1; собственный I/O-контракт вызываемого ADR-040 v.2.
Не-цели:
startQuantity/completionQuantity≠ 1 (§2.3) — неявное размножение токенов без нотации на диаграмме; явный Parallel Gateway покрывает намерение видимо.- Call Activity «выстрелил и забыл» (§2.7) — ни одна конструкция BPMN этого не выражает.
- Абзац §13.3.4 о старте, пересекающем границу (§2.2), — самопротиворечив с §7.6.1 и Table 7.2.
- Статическая проверка рекурсии (§2.9) — неразрешима между версиями реестра.
8. Ссылки¶
Проектные (вверх / вбок, с версиями):
- ADR-001 v.6 — ядро «трек/цикл, единственный писатель», которое это расширяет.
- ADR-010 v.2 §2.2 — дерево контейнерных областей, которое это задействует.
- ADR-018 v.1 — механизм прерывания, применённый к композитам.
- ADR-006 v.6 §2.2/§2.6/§2.7 — terminate и компенсация, модель распространения ошибок, реализованная здесь, и условный старт, размещённый §2.10.
- ADR-019 v.1 — реестр, против которого разрешается Call Activity.
- ADR-033 v.5 §2.10 — правило точности композитов, управляющее контрактом перезапуска §2.7.
- ADR-025 v.5.1 — модель итерации, едущая на этой области, и единица выполнения, приводящая в движение композит.
- ADR-026 v.1 — реестр компенсации, который несёт дерево областей.
- ADR-028 v.2 — вариант транзакции.
- ADR-035 v.1 — вариант ad-hoc.
- ADR-040 v.2 — объявленный I/O-контракт вызываемого и его валидация во время запуска.
- SAD-001 v.1.3 §15.3, §4 N8.
Стандарт (BPMN 2.0 KB): §7.6.1, Table 7.2, §10.4.1, §10.5.1, §10.5.2
(Table 10.85), §10.5.4, §10.5.6, §10.5.7, §13.3.1, §13.3.4, §13.5.4, §13.5.6 —
рабочие копии:
sub-processes.md,
data.md и
event-handling.md.
Открытые вопросы¶
Нет.
История документа¶
| Версия | Дата | Изменение |
|---|---|---|
| v.1 | 2026-07-16 | Первоначальная концепция, принята на своём приземлении. Композиция решена на ОДНОМ понятии — области выполнения как дереве внутри экземпляра: встроенный Sub-Process как узел-контейнер, открывающий дочернюю область в том же экземпляре (один цикл, единственный писатель сохранён, треки несут пути областей), с валидированными формами инстанциации, завершением по осушению, отменой области как единицей прерывания, включая областной Terminate, и обходом цепочки областей для Error. Call Activity — граница переиспользования: дочерний экземпляр callable, разрешаемого реестром, latest-at-launch по умолчанию, асинхронное ожидание для вызывающего, стандартное прямое отображение I/O, изолированная плоскость данных ребёнка и каскад terminate при отмене вызывающего. Абзац §13.3.4 о старте, пересекающем границу, отвергнут как самопротиворечивый с §7.6.1 и Table 7.2. |
| v.2 | 2026-07-17 | Event Sub-Process (§2.10), принят после приземления прерывающего среза: обработчик triggeredByEvent, взведённый пока открыта его объемлющая область, — паттерн boundary-watch, поднятый с окна активности на окно области, — переиспользующий пер-видовую машинерию триггеров и размещающий условный старт. Прерывающий старт отменяет братские треки своей области и выполняет обработчик в контексте данных родителя; прерывающий бюджет — один на Event Declaration и общий с boundary-событиями; absorb против re-throw даёт контроль «терминальный против декоратора». Непрерывающие обработчики порождаются параллельно в свежих дочерних областях. |
| v.3 | 2026-07-22 | Принято 2026-08-08. startQuantity/completionQuantity ≠ 1 переквалифицированы из отложенного в намеренную не-цель, а §2.7 получает контракт перезапуска: дочерний экземпляр долговечен сам по себе, чекпоинт вызывающего записывает вызов в полёте, восстановление поднимает оба конца и связывает их заново, а отсутствующая запись контрагента громко проваливает восстановление, вместо того чтобы дублировать или осиротить ребёнка. |
| v.4 | 2026-08-26 | Сторона вызываемого в отображении I/O переезжает в ADR-040 v.2, и на неё указывают, а не пересказывают: собственные объявленные входы и выходы процесса, связывание при запуске, чтение при завершении и валидация по именам параметров вызывающего против разрешённого callable во время запуска. Только указатели; семантика выполнения не менялась. |
| v.5 | 2026-08-28 | Ссылка на callable и её резолверный шов (§2.7): calledElement может быть квалифицирован пространством имён документа, объявившего callable; резолвер, поставляемый host'ом, превращает ссылку в ключ реестра во время вызова, вне всех блокировок движка; резолвер по умолчанию сохраняет неквалифицированный случай точным и отказывает квалифицированному по имени. §2.7 также решает семейство GlobalTask: global task — это callable-процесс, телом которого является эта одна задача, зарегистрированный под своим id, с ioSpecification callable в роли контракта процесса, так что переиспользование остаётся по ссылке. И то и другое — решения движка на почве, которую стандарт оставляет инструменту. Документ также переписан на актуальность: blockquote версии, план раскатки приземлённого среза и опережающие ссылки «когда приземлится» удалены, §2.8 становится регистром композитов, решаемых собственными ADR, §2.4 перестаёт предписывать, кто приводит в движение выполнение композита (ADR-025 v.5.1 §2.12 владеет этим), а сквозные pin'ы перепроверены. |