ADR-016 — Корреляция сообщений¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.1 |
| Дата | 2026-06-16 |
| Владелец | Ruslan Gabitov |
| Уточняет | ADR-001 v.5 Execution Model |
Принято — концепция решена; её первые фазы (2a — вывод ключа, 2b — инстанцирование с разрешением по ключу) реализованы, тогда как «протягивание» conversation-token (фаза 2c) и контекстная корреляция (фаза 3) остаются решёнными-но-отложенными (§2.8). Полностью фиксирует концепцию корреляции сообщений: как входящее сообщение сопоставляется с conversation/экземпляром, которому оно принадлежит, модель разрешения «сообщение → экземпляр» (маршрутизировать в существующий экземпляр, либо создать новый, либо придержать) и фазирование корреляции (ключевая — сейчас, протягивание conversation-token и контекстная корреляция — позже). Выделено из ADR-015 v.1, который сохраняет инстанцирование по событию; это два sibling-документа — ADR-015 владеет когда сообщение создаёт экземпляр, а данный ADR — какому экземпляру принадлежит сообщение. Обосновано BPMN 2.0 §8.4.2 (Correlation) и его смежными пунктами (§13.2 / §13.3.3 / §13.5.1). Реализующие SRD выполняют работу на уровне файлов и кода.
1. Контекст и проблема¶
Долгоживущий движок выполняет множество экземпляров одного и того же процесса параллельно — по одному на заказ, на клиента, на заявку. Когда приходит асинхронное сообщение, движок ДОЛЖЕН решить (§8.4.2, p72):
- создаёт ли это сообщение новый экземпляр или маршрутизируется в существующий?
- если в существующий — то в какой?
BPMN отвечает корреляцией: значения, извлечённые из самого payload-а
сообщения (orderID, customerID), определяют conversation, которому
принадлежит сообщение — без технических «липких» токенов. Фаза-1, сопоставление
по имени (ADR-014 §2.6) — сообщение достигает waiter-а, подписанного на то же
имя сообщения — не способна различить два выполняющихся процесса-заказа, оба
ждущие "payment received". Корреляция и есть тот различитель.
Это было объединено в ADR-015, пока непосредственной целью была работа по инстанцированию. Корреляция — крупная тема сама по себе: объектная модель из пяти элементов, два неисключающих механизма (ключевой, контекстный), модель идентичности conversation и инвариант «не более одного получателя на ключ» — и она пронизывает все конечные точки сообщений (задачи и события; §8.4.2, сноска 1), а не только инстанцирование. Она заслуживает собственного ADR, чтобы концепция была полной, а отложенные части (протягивание conversation-token, контекстная корреляция) получили именованный концептуальный дом, а не жили как ad-hoc сужения в SRD.
1.1 Корреляция и инстанцирование — один алгоритм, два ADR¶
BPMN §8.4.2 формулирует единый алгоритм разрешения «сообщение → экземпляр»: корреляция сопоставляет сообщение с conversation/экземпляром; если совпадения нет, а сообщение может инстанцировать — создаётся новый экземпляр. Инстанцирование — это ветка «нет существующего совпадения, но можно инстанцировать».
Мы разделяем документацию по концептуальному шву, а не алгоритм:
- Данный ADR (корреляция) — какому экземпляру принадлежит сообщение: модель ключа, вывод/сопоставление, решение о разрешении, идентичность conversation.
- ADR-015 (инстанцирование по событию) — само создание экземпляра при срабатывании стартового триггера: instance-starter уровня определения, born-from-event-посев, manual-start.
Алгоритм разрешения (§2.3) принадлежит сюда, потому что это решение корреляции; starter из ADR-015 это решение потребляет.
2. Решение¶
2.1 Объектная модель — стандартная, дословно¶
gobpm моделирует корреляцию пятью элементами BPMN ровно так, как они заданы (§8.4.2, Tables 8.31–8.35), сохраняя стандартную таксономию:
flowchart TD
CK["CorrelationKey\n(name; the composite routing key)"]
CP["CorrelationProperty\n(name, type; a partial key)"]
RE["CorrelationPropertyRetrievalExpression\n(messagePath, messageRef)"]
CS["CorrelationSubscription\n(process-specific)"]
CB["CorrelationPropertyBinding\n(dataPath, propertyRef)"]
CK -->|"correlationPropertyRef 0..*"| CP
CP -->|"retrieval expr 1..* (one per Message type)"| RE
CS -->|"correlationKeyRef"| CK
CS -->|"propertyBinding 0..*"| CB
CB -->|"propertyRef"| CP
- CorrelationKey — составной ключ из одного или нескольких частичных ключей CorrelationProperty. Ключ действителен только когда все его свойства заполнены (§8.4.2; движок ДОЛЖЕН отслеживать состояние заполнения каждого свойства).
- CorrelationProperty — частичный ключ; несёт по одному CorrelationPropertyRetrievalExpression на каждый тип сообщения в conversation.
- CorrelationPropertyRetrievalExpression —
messagePath(FormalExpression, извлекающий значение из payload-аmessageRef) +messageRef(сообщение, к которому он применяется). - CorrelationSubscription / CorrelationPropertyBinding — со стороны
процесса, аналог для контекстной корреляции: выражения
dataPathнад контекстом процесса, а не над payload-ом сообщения (§2.5).
2.2 Ключевая корреляция (основной механизм)¶
Простой, эффективный механизм (§8.4.2, p74–75). Conversation идентифицируется
одним или несколькими CorrelationKey; обе стороны выводят один и тот же
составной ключ из общей структуры payload-а.
- Заполнение (производитель / первый участник). Первый send или receive в
conversation заполняет ключ, вычисляя retrieval-выражение каждого свойства
(чей
messageRefсовпадает с сообщением в полёте) над payload-ом сообщения. Производитель несёт выведенный составной ключ на исходящем сообщении («joint conversation token», §8.4.2). - Сопоставление (потребитель). Входящее сообщение выводит свой составной
ключ тем же способом —
messagePathкаждого свойства (выбранный поmessageRef) вычисляется над входящим payload-ом — и выведенный ключ должен совпасть с инициализированным ключом conversation, чтобы туда маршрутизироваться. - Всё-или-ничего. Составной ключ с любым неразрешённым свойством недействителен и не совпадает ни с чем — частичного совпадения не бывает.
- Не более одного получателя на ключ (§13.3.3). При ключевой корреляции
движок НЕ ДОЛЖЕН иметь два активных получателя на один и тот же
CorrelationKeyодновременно; сообщение совпадает не более чем с одним экземпляром. (Контекстная корреляция ослабляет это до fan-out — §2.5.)
2.3 Модель разрешения «сообщение → экземпляр»¶
Каждое входящее сообщение разрешается одинаково, независимо от того, заканчивается ли оно на задаче или на событии (§8.4.2, сноска 1 — send/receive задачи и throw/catch события сообщений ведут себя для корреляции одинаково):
flowchart TD
M[("incoming message (payload, derived key)")] --> Q{"a receiver waits on (name, key)?"}
Q -->|"yes"| R["route to that instance's parked receiver (in-instance wait, ADR-014)"]
Q -->|"no"| I{"can this message instantiate? (message start / instantiate ReceiveTask)"}
I -->|"yes"| E{"an instance already exists for this key?"}
E -->|"no"| N["create a new instance (born from the trigger), record the key"]
E -->|"yes"| J["join the existing instance (no duplicate)"]
I -->|"no"| H["no target: hold (bounded broker inbox) or drop"]
- «Создать-или-маршрутизировать» атомарно по ключу (§13.5.1). Два сообщения
с одним и тем же ещё-не-существующим ключом НЕ ДОЛЖНЫ каждое порождать
экземпляр — разрешение по
(name, key)это single-flight: первое создаёт, конкурентные/последующие стартовые триггеры с тем же ключом присоединяются к этому экземпляру. «Новый или существующий» и сам акт создания — один атомарный шаг по ключу корреляции, а не check-then-act. - Специфичность: получатель с ключом побеждает wildcard-starter. Когда и получатель существующего экземпляра с ключом, и instance-starter уровня движка могли бы принять сообщение, побеждает получатель с ключом (маршрутизация в существующий экземпляр); starter инстанцирует только когда ни один получатель с ключом не ждёт. Именно это делает выбор «в существующий или инстанцировать» детерминированным, а не зависящим от порядка регистрации.
- Нет цели → забота брокера (§2.7). Если ничто не совпало, а сообщение не может инстанцировать, распоряжение (придержать/отбросить/TTL) — политика брокера.
2.4 Протягивание conversation-token (решено; реализация фазирована)¶
Полная модель conversation — это совместный токен, передаваемый туда-обратно в каждом сообщении обмена (§8.4.2): ключ инициализируется первым send/receive и сопоставляется на каждом последующем сообщении; последующее сообщение, чей ключ уже был инициализирован, ДОЛЖНО совпасть со значением conversation (несовпадение = нет маршрутизации), а последующее, выводящее ещё-не- инициализированный вторичный ключ, лениво ассоциирует это значение с conversation.
Концептуально решено здесь. Два следствия для движка:
- Выполняющийся экземпляр несёт свой conversation-ключ (или ключи); его
in-instance-получатели сопоставляют входящие сообщения по
(name, key), так что последующее сообщение маршрутизируется в конкретный экземпляр, чьему conversation оно принадлежит (§2.3, специфичность). - Ленивая инициализация вторичного ключа и многоключевая слоистая маршрутизация — часть этой модели.
Фазирование. Маршрутизация последующего сообщения в конкретный уже выполняющийся экземпляр через его получателя с ключом (полное протягивание токена) отложена в следующий SRD; первая реализация landing-ит инстанцирование с разрешением по ключу (решение create-or-route-or-join у starter-а, §2.3), которое уже реализует «два параллельных экземпляра, различаемых по ключу» и «последующий старт присоединяется к существующему, без дубликата». Отсрочка — решённая фаза этой модели, а не сужение — см. §2.8.
2.5 Контекстная корреляция (решено; отложено)¶
Более выразительный механизм (§8.4.2 p76; «predicate-based» в §13.3.3),
построенный поверх ключевой и неисключающий с ней. Процесс предоставляет
CorrelationSubscription, чьи выражения CorrelationPropertyBinding.dataPath
вычисляются над контекстом процесса (data objects / properties), а не над
payload-ом сообщения. Он реактивен: при изменении ссылаемого элемента данных
подписка перевычисляется, так что процесс может перенацелить на ходу, какие
сообщения он принимает. В отличие от ключевой (не более одного получателя на
ключ), predicate-based МОЖЕТ доставить сообщение нескольким получателям
(fan-out; сообщение не «съедается» после первой доставки). Решено как концепция;
реализуется после ключевой.
2.6 Где объявляются ключи корреляции — без Conversation (заметка движка)¶
В BPMN CorrelationKey принадлежит Conversation (§8.4.2 / §9.5.1). Полная
метамодель Conversation / Collaboration (Pool, Participant,
ConversationNode) вне области Process-Execution-Conformance, однако
логический conversation — группировка сообщений, разделяющих ключ, область
идентичности обмена — упоминается правилами маршрутизации экземпляров (§13.2 /
§13.5.1) и не может быть чисто вырезан.
Выбор движка (намеренное, обоснованное стандартом отклонение): gobpm
объявляет CorrelationKey на уровне процесса (стандарт уже привязывает
ключи к процессу через CorrelationSubscription), а стартовое событие
сообщения / получатель / отправитель ссылается на ключ, по которому он
коррелирует. Объектная модель стандарта сохранена дословно (§2.1) — заменён
лишь контейнер (элемент Conversation) на процесс. Логический conversation
выживает как «привязка между конечными точками сообщений процесса и внешним
миром». Контейнер Conversation остаётся штатной отдушиной стандарта, если
когда-либо понадобится кросс-процессная группировка (отложено).
2.7 Сообщения без цели — забота брокера (ограниченный буфер)¶
Разрешение даёт существующий экземпляр, новый или отсутствие цели. Стандарт молчит о случае «нет цели» (§8.4.2: движок «drops or holds the message per implementation policy»). Данный ADR владеет разрешением, а не временем жизни сообщения: распоряжение сообщением без цели (drop / hold / TTL / dead-letter) и то, как придержанное сообщение достигает позднего потребителя — заботы брокера (ADR-002 v.1 / будущий ADR Distribution & Scale). Два свойства держатся независимо от брокера:
- Придерживание ДОЛЖНО быть ограниченным — брокер, удерживающий сообщения без цели, ДОЛЖЕН ограничивать удержанное (по числу и/или памяти) и вытеснять сверх этого, чтобы backlog никогда не исчерпал память (принцип bounded-in-memory-defaults, ADR-002). In-memory-брокер по умолчанию уже ограничивает свой inbox и отбрасывает самые старые сверх предела.
- Доставка — pull-on-subscribe (текущий дефолт) — придержанное сообщение пере-рассматривается только когда подписывается совпадающий потребитель (in-instance-получатель, когда до него доходит токен; instance-starter при регистрации процесса), который сливает совпадающие буферизованные сообщения. Фонового sweeper-а нет.
Распоряжение «нет цели» задумано как настраиваемая политика брокера: отбрасывать или хранить, сколько (число удержания), как долго (TTL). Нижняя граница по числу не обсуждается; TTL и режим drop/keep — выбор оператора. Проектирование этих ручек — дело брокера (ADR-002 / ADR Distribution & Scale), а не данного ADR. Издателю ошибка не возвращается (на этом слое — fire-and-forget).
2.8 Фазирование¶
flowchart LR
P1["Phase 1 — name match\n(done, ADR-014)"] --> P2a["Phase 2a — key derivation\n(producer sets / consumer derives a composite key)"]
P2a --> P2b["Phase 2b — key-based instantiation-resolution\n(create-or-route-or-join by key at the starter)"]
P2b --> P2c["Phase 2c — conversation-token threading\n(route follow-up messages to a specific running instance)"]
P2c --> P3["Phase 3 — context-based correlation\n(predicate over process context; fan-out)"]
- Фаза 1 (готово) — сопоставление по имени (ADR-014 §2.6).
- Фаза 2a (готово) — вывод составного ключа из payload-а (все свойства обязательны) и установка производителем выведенного ключа на исходящем сообщении.
- Фаза 2b (далее) — инстанцирование с разрешением по ключу: starter выводит входящий ключ и делает атомарный create-or-route-or-join (§2.3); производитель несёт ключ. Реализует «два экземпляра, различаемых по ключу» и «последующий старт присоединяется к существующему».
- Фаза 2c (отложено) — протягивание conversation-token (§2.4): in-instance-получатели с ключом + маршрутизация по специфичности, чтобы последующее сообщение достигало конкретного выполняющегося экземпляра; ленивая инициализация вторичного ключа.
- Фаза 3 (отложено) — контекстная корреляция (§2.5).
2.9 Не-цели (у каждой — именованный дом)¶
- Метамодель
Conversation/Collaboration(Pool,Participant, …) — вне области conformance; ключи объявляются на процессе (§2.6). - Корреляция инстанцирования через event-based gateway (§13.4.4 / §10.6.6, включая ограничение одинаковой корреляции parallel-event-gateway) — нужен узел event-based gateway; веха реализации шлюзов.
- Реализация контекстной / predicate-корреляции — решено (§2.5), реализуется после ключевой.
- Реализация протягивания conversation-token — решено (§2.4), фаза 2c.
- Долговечное состояние корреляции между перезапусками — ADR по Persistence.
- Гарантии межэкземплярной доставки, упорядочивание, dead-letter — заботы качества брокера (ADR-002 / ADR Distribution & Scale).
3. Последствия¶
- У корреляции единый концептуальный дом; ADR-015 (инстанцирование) и ADR-014 (обработка сообщений) ссылаются на неё вбок за каким экземпляром.
- Модель разрешения (§2.3) объединяет in-instance-ожидание (ADR-014) и инстанцирование (ADR-015): одно дерево решений, три исхода (маршрутизировать / создать / придержать). Нет параллельного пути корреляции на каждый вид конечной точки (§8.4.2, сноска 1).
- Фазированная модель позволяет инстанцированию с разрешением по ключу приземлиться сейчас, тогда как протягивание conversation-token и контекстная корреляция остаются решёнными, а не импровизируемыми, когда придут их SRD.
- Инвариант «не более одного получателя на ключ» (§13.3.3) и действительность ключа «все свойства обязательны» становятся обязательствами движка, которые реализующие SRD должны соблюдать.
4. Рассмотренные альтернативы¶
| Альтернатива | Почему отвергнута |
|---|---|
| Корреляция в брокере (брокер выводит ключи и маршрутизирует в экземпляры) | Брокер — транспортная граница (ADR-002), намеренно model-agnostic — он сопоставляет лишь по имени + непрозрачному ключу. Размещение извлечения payload-а + модели conversation в брокере связывает транспорт с объектной моделью BPMN и блокирует альтернативные брокеры. Корреляция — забота движка; брокер несёт непрозрачный выведенный ключ. |
| Потребитель слепо доверяет ключу, установленному производителем (без вывода на стороне потребителя) | Проще, но ломает симметрию стандарта — потребитель ДОЛЖЕН выводить по своим retrieval-выражениям для сопоставления (§8.4.2), и внешний производитель может вообще не выставлять ключ. Установленный производителем ключ — оптимизация на проводе; потребитель всё равно выводит для валидации/маршрутизации. (Starter фазы 2b выводит из payload-а; он не доверяет ключу с провода слепо.) |
| Технические «липкие» ID корреляции (conversation-токены, назначаемые движком) | Ровно то, чего корреляция BPMN избегает (§8.4.2): корреляция использует бизнес-значения из payload-а, чтобы не навязывать участникам внеполосную возню с токенами. |
| Оставить корреляцию внутри ADR-015 | Смешивает две заботы; отложенные части (протягивание токена, контекстная) не имели концептуального дома и всплывали как ad-hoc сужения в SRD. Sibling-ADR держит каждый ADR самодостаточным (это выделение). |
| Одна объединённая «фаза 2 корреляции» (ключевая + протягивание токена вместе) | Слишком крупно и рискованно для одного landing-а; протягивание токена требует in-instance-получателей с ключом + маршрутизации по специфичности + проброса ключа через шов регистрации. Фазирование 2a/2b/2c приземляет проверяемую ценность рано. |
5. Рекомендации по enterprise-готовности¶
- Наблюдайте исходы корреляции, но не payload-ы. Эмитьте структурированное
событие на каждое разрешение —
name, выведенный ключ (или его хеш), исход (routed / created / joined / held) и id целевого экземпляра — но никогда не значения payload-а и не сырые компоненты ключа (они несут бизнес-PII). Это тот аудит-след, который операторам нужен для отладки неверной маршрутизации. - Сделайте разделитель/хеширование ключа явным и стабильным. Составной ключ — контракт между независимо развёрнутыми производителями и потребителями; задокументируйте правило соединения/нормализации, чтобы производитель и потребитель, собранные раздельно, выводили идентичные ключи.
- Выведите инвариант «один получатель на ключ» в валидацию. Модель с двумя активными ключевыми получателями на один ключ некорректна (§13.3.3); отмечайте это при регистрации, где возможно, и охраняйте в рантайме, когда MI-активности могут порождать получателей динамически.
- Относитесь к удержанию сообщений как к операционному SLO. Ограниченный inbox (§2.7) отбрасывает самые старые под давлением; выставьте его глубину/число вытеснений метрикой, чтобы операторы видели backlog корреляции до того, как он станет тихой потерей сообщений.
- Планируйте долговечное состояние корреляции. In-memory ключи/подписки теряются при перезапуске; развёртывание, которое должно переживать перезапуски, нуждается в ADR по Persistence прежде, чем полагаться на долгие conversation.
6. Ссылки¶
- BPMN 2.0 §8.4.2 (Correlation, pp.72–78; Tables 8.31–8.35) и смежные пункты §13.2 / §13.3.3 / §13.5.1 (инстанцирующий старт, receive task, start event), §13.4.4 / §10.6.6 (event-based gateway), §9.5.1 (Conversation, только контекст) — управляющий стандарт, через вендоренную выжимку.
- ADR-015 v.1 Event-triggered instantiation — sibling; потребляет решение о разрешении (§2.3) в instance-starter.
- ADR-014 v.1 Message Handling — шов
производитель/потребитель,
MessageBroker/MessageWaiterи поле ключаEnvelope, на которых эта корреляция едет. - ADR-006 v.1 Events & Subscriptions — принадлежащий EventHub жизненный цикл waiter-ов, который расширяют получатели с ключом.
- ADR-002 v.1 Extension Architecture —
граница
MessageBrokerи bounded-in-memory-дефолты (§2.7). - ADR-001 v.5 Execution Model — экземпляры/треки, которые питает модель разрешения.
7. Открытые вопросы¶
Нет.
История документа¶
| Версия | Дата | Изменение |
|---|---|---|
| v.1 (Принято) | 2026-06-16 | Принято — концепция решена; фазы 2a (вывод ключа) и 2b (инстанцирование с разрешением по ключу) реализованы landing-SRD; фаза 2c (протягивание conversation-token) и фаза 3 (контекстная корреляция) решены-но-отложены (§2.8). |
| v.1 | 2026-06-16 | Первичный черновик. Выделил концепцию корреляции из ADR-015 v.1 (объектная модель, ключевой механизм, модель разрешения, протягивание conversation-token, контекстная корреляция, объявление ключей без Conversation, нет-цели/ограниченный буфер) и добавил фазирование (2a/2b/2c/3). |