SRD-015 — Key-based correlation & event-triggered instantiation¶
| Field | Value |
|---|---|
| Status | Accepted |
| Version | v.1 |
| Date | 2026-06-16 |
| Owner | Ruslan Gabitov |
| Implements | ADR-015 v.1 Event-triggered instantiation + ADR-016 v.1 Message correlation |
This SRD lands event-triggered instantiation (ADR-015 v.1) — a message creates a process instance — and key-based correlation (ADR-016 v.1, phase-2a/2b) — a message routes to the right instance, or a new one, by a key derived from its payload. It builds on the message tasks/events of ADR-014 (SRD-013/014) and uses the BPMN §8.4.2 correlation model and §13.2/§13.5.1/§13.3.3 instantiation semantics. Conversation-token threading (ADR-016 §2.4, phase-2c), context-based/predicate correlation (ADR-016 §2.5), the event-based-gateway start, Conversation, and durability stay deferred.
1. Background & motivation¶
1.1 Current state (verified against the code)¶
- Messaging is in-instance only, routed by name.
pkg/model/msgflow.Sendpublishes anEnvelope{Name, Payload}withCorrelationKeyleft empty (send.go:49);membrokermatches on name + (empty-or-equal key) (membroker/membroker.go:40); aMessageWaitersubscribes with an empty key (waiters/message.go:176). Every receiver runs inside an already-started instance. - No event-triggered instantiation.
Thresher.RegisterProcessonly builds + stores a snapshot (thresher.go:364); the only instance-creation path isThresher.StartProcess → launchInstance → instance.New + Run(thresher.go:394,422). There is noUnregisterProcess(thresher.go— none). A message that should spawn a process has no target. createTrackseagerly parks a message start event.instance.createTracksseeds an initial track for every no-incoming, non-gateway, non-boundary node (instance.go:472) — including a messageStartEvent(no incoming) and aninstantiateReceiveTask. Reached as anEventNode+EventProcessor,track.checkNodeTyperegisters a waiter and parks it (track.go:283) — an instance existing before its trigger (ADR-015 §1).- The
MessageWaiteris one-shot. It reads a single envelope and self-removes (waiters/message.go:209,245-250); itsfireDefinition(message.go:260) reconstructs the payload as a typed Ready datum — reusable. An instantiating subscription must instead be persistent (every message spawns another instance; ADR-015 §2.2). - The correlation model already exists as pure data.
pkg/model/bpmncommon/correlation.godefinesCorrelationKey(:71),CorrelationProperty(:83),CorrelationPropertyRetrievalExpression{MessagePath, MessageRef}(:111),CorrelationSubscription(:51),CorrelationPropertyBinding(:104) — no behaviour, no constructors.process.Process.CorrelationSubscriptionsholds them (process/process.go:37). Missing: builders, a way to attach a key to a message trigger, and the runtime derivation logic. - Expressions evaluate over a
data.Source.EngineRuntime.ExpressionEngine().Evaluate(ctx, expr, src)(expression/expression.go:21) — but there is no "evaluate over a raw payload" path; aMessagePathmust run against the message payload, so a payload→data.Sourceadapter is new. ReceiveTask.Instantiateexists but is unset. The field + getter exist (activities/receive_task.go:42,103), always false; there is noWithInstantiateoption.- The engine EventHub is engine-level and node-agnostic. The Thresher owns one
eventhub.New(&t.cfg)(thresher.go:149);Thresher.RegisterEventdelegates to it (thresher.go:290); theMessageWaiterfires anyeventproc.EventProcessor. So a definition-level starter is just a differentEventProcessoron the same hub.
1.2 Why¶
ADR-015 decided the model; without it gobpm cannot start a process from a message, and cannot run more than one instance per message name (name-match can't tell "payment for order 42" from "payment for order 99"). Long-running business processes need both: a message spawns an instance, and subsequent messages correlate to the right running one. The pieces exist (correlation structs, broker key field, node-agnostic waiter, expression engine); this SRD wires them.
2. Goals & scope¶
2.1 Goals (in scope)¶
- G1. A persistent engine-level message subscription (peer of the one-shot in-instance waiter) that fires an
eventproc.EventProcessorfor every matching message without self-removing. - G2. A Thresher-hosted start-subscription manager: at
RegisterProcessit registers an instance-starter (anEventProcessor) per instantiating start trigger; a newUnregisterProcesstears them down. - G3. Event-triggered instantiation for the message start event and the instantiate
ReceiveTask(no incoming flow): on a matching message the starter creates a new instance, born with the start node already fired and the payload bound, and runs it.createTracksstops seeding instantiating start triggers; aWithInstantiateoption setsReceiveTask.instantiate. - G4. Key-based correlation: a
CorrelationKeyderived from the message payload via itsCorrelationProperty/CorrelationPropertyRetrievalExpression.MessagePath; the producer setsEnvelope.CorrelationKey; the resolution routes a message to the existing correlated instance if one waits, otherwise to a new one (instantiation is the no-match branch). Builders for the correlation structs + a process-level key declaration (Conversation-less — see §4.5). - G5. A runnable example: process A sends a message that starts / routes to process B by correlation key (the inter-instance demo).
2.2 Non-goals (deferred, per ADR-015 §2.6)¶
- Context-based / predicate correlation (
CorrelationSubscriptiondataPathover process context, dynamic re-targeting) — later SRD. - Event-based-gateway start (the node type isn't implemented).
Conversation— out of conformance scope; keys are declared without it (§4.5).- Durable subscriptions / persistence across restart; broker-quality TTL / dead-letter / ordering (broker / the Distribution & Scale ADR). The held-message buffer stays bounded + pull-on-subscribe (ADR-015 §2.5).
- Composite multi-message conversation-token threading beyond a single derived key (key init-on-first + match-on-arrival is in; the full back-and-forth token is later).
3. Requirements¶
3.1 Functional¶
| # | Requirement |
|---|---|
| FR-1 | The existing message waiter gains a constructor flag (single-shot vs persistent — e.g. singleShot bool); no new waiter type. The waiter never removes itself — the EventHub is the sole remover (ADR-006 v.1 §2.5). After a waiter fires it reports completion to the hub (a terminal state the hub reaps, or a hub callback — not a RemoveWaiter call from the waiter); the hub then removes it if single-shot (the in-instance receiver — net behaviour unchanged: gone after one fire) and retains it if persistent (the instance-starter — fires on every matching envelope, stays until UnregisterProcess → UnregisterEvent, Stop, or ctx). This corrects the current waiter self-removal (waiters/message.go:250) to the ADR-006 hub-owned model. fireDefinition is shared by both modes. |
| FR-2 | Thresher gains a start-subscription manager: RegisterProcess scans the snapshot for instantiating start triggers (a StartEvent carrying a MessageEventDefinition with no incoming flow; a ReceiveTask with instantiate=true and no incoming flow) and registers a persistent instance-starter (EventProcessor) per trigger on the engine EventHub; UnregisterProcess removes them. |
| FR-3 | The instance-starter's ProcessEvent creates a new instance via a born-from-event path: the start node is treated as already fired, the payload is bound as its output, and the initial track starts on the start node's outgoing flow(s). createTracks no longer seeds instantiating start triggers (they instantiate via the starter, not as eager parked tracks). A none start event keeps the StartProcess path. |
| FR-4 | ReceiveTask gains a WithInstantiate option setting instantiate=true; a no-incoming instantiate ReceiveTask participates in FR-2/FR-3 like a message start event. |
| FR-5 | Key derivation: given a CorrelationKey's CorrelationProperty set and a message payload, compose the composite key by evaluating each property's CorrelationPropertyRetrievalExpression.MessagePath (whose MessageRef matches the message) over a payload-backed data.Source, via the ExpressionEngine. A key is valid only when all its properties resolve. msgflow.Send sets Envelope.CorrelationKey from the producer's correlation declaration. |
| FR-6 | Resolution (phase-2b): an incoming message derives its key; the starter does create-or-route-or-join per key — an unseen key creates a new instance, a seen key joins the existing one (a subsequent start sharing the key does not duplicate), and a message with no derivable key instantiates as before; an unrouteable message is held (bounded, pull-on-subscribe — ADR-015 §2.5). Deferred to phase-2c (ADR-016 §2.4/§2.8): routing a follow-up message to a specific running instance's keyed in-instance receiver (the conversation-token threading) — in-instance receivers still subscribe by name in this SRD. |
| FR-7 | Builders/options for the correlation structs and a process-level CorrelationKey declaration that a message start event / receiver references (Conversation-less, §4.5); no internal/* import from pkg/model. |
| FR-8 | A runnable example (own module): process A publishes a message that instantiates/routes to process B by correlation key; exits 0, proving inter-instance correlation. |
| FR-9 | Thresher.RegisterProcess accepts options; WithManualStart() registers a process manual-start: no persistent instance-starter is registered for it (no message spawns an instance — opt-out of auto-instantiation, for testing / back-pressure, ADR-015 §2.2 engine note). Such a process is instantiated only via StartProcess, and inside that instance its instantiating start nodes are not skipped by createTracks — they are seeded as ordinary in-instance catches (the intermediate-node rule). Default (no option) is unchanged: auto-instantiation as FR-2/FR-3. The skip in FR-3 is therefore mode-driven (auto skips instantiating starts; manual seeds them). |
3.2 Non-functional¶
| # | Requirement |
|---|---|
| NFR-1 | No payload values in logs — message name, key (or its hash), item ids, states only (ADR-010/011/014; ADR-015 §5 sensitive-keys). |
| NFR-2 | Bounded held-message buffer (no-OOM, ADR-015 §2.5); the persistent starter subscription must not leak goroutines/subscriptions — torn down at UnregisterProcess and engine shutdown. -race clean. |
| NFR-3 | make ci green per milestone; diff-coverage ≥95 % (target 100 %) on touched files; existing thresher / instance / eventhub / model suites pass. |
| NFR-4 | pkg/model imports no internal/* (depguard); new exported symbols documented; new constructors/options validate inputs with self-identifying errors. The instance-starter lives in the Thresher (a focused collaborator), never on Instance (ADR-015 §2.2). |
4. Design & implementation plan¶
4.1 One resolution path, two event-processor kinds¶
flowchart TD
M[("message (Envelope: name, payload)")] --> K["derive CorrelationKey from payload (MessagePath via ExpressionEngine)"]
K --> Q{"a receiver waits on (name, key)?"}
Q -->|"yes"| R["in-instance wait: one-shot MessageWaiter -> track.ProcessEvent (ADR-014)"]
Q -->|"no"| S{"an instantiating start trigger matches (name, key)?"}
S -->|"yes"| N["instance-starter (persistent) -> new instance, start node pre-fired + payload"]
S -->|"no"| H["held: bounded inbox, pull-on-subscribe (ADR-015 §2.5)"]
The in-instance receiver (one-shot waiter → track) is ADR-014/SRD-013-014. This SRD adds the instance-starter (persistent waiter → new instance) and the key derivation feeding both.
4.2 One waiter, a single-shot/persistent flag — the hub owns removal¶
The existing messageWaiter gets a constructor flag (single-shot vs
persistent) — no new type — and never calls RemoveWaiter (correcting the
current waiters/message.go:250 self-removal). The EventHub is the sole
lifecycle owner (ADR-006 v.1 §2.5): after a waiter fires, it reports
completion to the hub (a terminal state the hub reaps, or a hub callback), and
the hub removes it when single-shot and keeps it when persistent.
- Single-shot (the in-instance receiver): net behaviour unchanged — removed by the hub after one fire; today it self-removed, now the hub removes it.
- Persistent (the instance-starter): loops the subscription, fires for every
matching message, is never reaped on fire; the hub drops it only at
UnregisterProcess → UnregisterEvent,Stop, or ctx.
It subscribes for (name, derived-key) (or (name, "") and filters by key).
fireDefinition is shared. The hub holds or drops the waiter purely per the
flag; the waiter is removal-passive. Removal is unified across every waiter
(not just the message one): a new EventHub.WaiterFired(eDefID) is the single
removal entry point — a waiter reports its fire and the hub removes it iff
the waiter is in a terminal state (WSEnded/WSFailed), keeping a still-running
one (a persistent message waiter, or a timer mid-cycle). The timer waiter is
migrated in the same milestone from its own RemoveWaiter self-call to
WaiterFired, so no waiter self-removes — fully realizing ADR-006 v.1 §2.5 for
the whole waiter family, not a message-only slice.
4.3 The instance-starter & start-subscription manager (Thresher)¶
flowchart LR
RP["RegisterProcess(def, opts...)"] --> MS{"WithManualStart?"}
MS -->|"yes (manual)"| SK["no starter registered\n(StartProcess-only)"]
MS -->|"no (auto, default)"| SC["scan snapshot: instantiating start triggers\n(message StartEvent / instantiate ReceiveTask, no incoming)"]
SC --> REG["register a persistent instance-starter per trigger on the engine EventHub"]
B[("MessageBroker")] -->|"message"| W["persistent waiter"]
W -->|"ProcessEvent"| ST["instanceStarter"]
ST --> LI["Thresher.launchInstanceFromEvent(snapshot, startNode, payload)"]
LI --> RUN["new instance runs"]
UP["UnregisterProcess(def)"] --> RM["UnregisterEvent -> RemoveWaiter (teardown)"]
A focused collaborator owned by the Thresher (a struct field, built in New), wired into RegisterProcess/UnregisterProcess. The starter implements eventproc.EventProcessor; its ProcessEvent calls a new launchInstanceFromEvent (sibling of launchInstance). Never touches Instance. The scan runs only in auto mode (the default); a WithManualStart-registered process registers no starter (FR-9, ADR-015 §2.2 engine note) — its lifecycle stays StartProcess-only. Because RegisterProcess may be called before Run (the hub starts in Run), starters are registered on the hub at the later of RegisterProcess/Run, and torn down at UnregisterProcess.
4.4 Born-from-event instantiation (mode-driven createTracks skip)¶
createTracks gains a predicate to skip a no-incoming node that is an instantiating start trigger so long as the instance is born from an event (auto mode): such a node does not auto-park because the starter pre-fires it. A new instance-creation entry (instance.NewFromEvent, or a parameter to New) builds the instance with that start node already fired: its payload is bound as its output (reuse the catchEvent dataOutput path / msgflow.Bind), and the initial track starts on the start node's outgoing flow(s) — analogous to fork seeding (instance.go:396) — bypassing track.checkNodeType parking. In manual mode (FR-9) the skip does not apply: the instantiating start node is seeded as an ordinary in-instance catch (StartEvent embeds catchEvent, so track.checkNodeType parks it and registers a single-shot waiter — event.go:212), and the instance waits for its message after an explicit StartProcess. A none start event is unaffected in either mode (still StartProcess).
4.5 Key derivation & Conversation-less key declaration¶
flowchart LR
P[("payload (Envelope.Payload)")] --> A["payload -> data.Source adapter"]
A --> E["ExpressionEngine.Evaluate(MessagePath, source) per CorrelationProperty"]
E --> C["compose composite CorrelationKey (all properties required)"]
C --> EK["Envelope.CorrelationKey (producer) / match key (consumer)"]
- Derivation — a runtime helper takes a
CorrelationKey(itsCorrelationPropertys, each with aCorrelationPropertyRetrievalExpressionselected byMessageRef) and a payload, and composes the composite key string via theExpressionEngineover a payload-backeddata.Source(new adapter, mirroring howfireDefinitionreconstructs the payload as a typed datum). The producer (msgflow.Send) setsEnvelope.CorrelationKey; the consumer/starter derives the same key to match. - Where keys live (engine note — owned by ADR-016 v.1 §2.6). In BPMN,
CorrelationKeys belong to aConversation(§8.4.2); gobpm keepsConversationout of scope and declares keys at the process level instead (the structs already hang off the process), preserving the standard's key/property/retrieval object model verbatim — only the container is replaced. A message start event / receiver references the key it correlates on.
4.6 Milestones (each = one commit, make ci green)¶
- M1 — single-shot/persistent waiter flag (hub-owned removal, unified). Add the constructor flag to the existing message waiter and move removal from the waiter to the EventHub via a new
EventHub.WaiterFired(eDefID)(the hub removes a waiter only when it reports a terminal state — single-shot after one fire, never a persistent one — ADR-006 v.1 §2.5) +NewMessageWaiter/CreateWaiterwiring. Unify all waiters now: migrate the timer waiter off itsRemoveWaiterself-call toWaiterFiredtoo, so no waiter self-removes. Unit tests (single-shot removed by the hub as before; persistent fires repeatedly, retained; timer still removed after its last cycle, now via the hub;WaiterFiredreaps terminal / keeps running). - M2 — instance-starter + manager + registration option.
EventHubgains a persistent registration path (RegisterPersistentEvent+waiters.CreatePersistentWaiter); Thresher collaborator scans atRegisterProcess(auto mode only), registers persistent starters at the later ofRegisterProcess/Run,UnregisterProcesstears down.RegisterProcess(p, ...RegisterOption)+WithManualStart()(FR-9) suppresses the scan. Tests (registration wires/teardowns subscriptions; manual-start registers none). The actual launch (launchInstanceFromEvent) is a one-commit placeholder filled in M3. - M3 — born-from-event instantiation + mode-driven
createTracksskip.instance.NewFromEvent+Thresher.launchInstanceFromEvent; a message start event spawns an instance that runs from the start node with the payload.createTracksskips instantiating starts for born-from-event (auto) instances and seeds them as catches otherwise (FR-9). Instance integration tests (auto: publish → new instance completes, payload in scope; manual:StartProcess→ instance waits at the start node, then publish → completes). - M4 — instantiate
ReceiveTask.WithInstantiateoption; a no-incoming instantiate receiver instantiates like a message start event. Tests. - M5a — correlation key derivation & producer. Correlation builders (
NewCorrelationKey/Property/RetrievalExpression);msgflow.DeriveKey+ a payload→data.Sourceadapter (composite key from a payload, all properties required). Tests (composite-key derivation; partial key invalid;MessageRefselection). (msgflow.Sendwiring lands with the producer declaration in M5b.) - M5b-consumer — key-based instantiation-resolution (ADR-016 v.1 §2.3, phase-2b).
WithCorrelationKeyon the message start trigger; the starter derives the incoming key from the payload and the manager does atomic create-or-route-or-join per key (empty key → instantiate as before; non-empty unseen → instantiate + record; seen → join, no duplicate). Tests (distinct keys → distinct instances; same key → one; no key → each instantiates). - M5b-producer — producer sets the key.
WithCorrelationKeyonSendTask;msgflow.Sendderives + setsEnvelope.CorrelationKey. Test (producer key == starter-derived key round-trip). - (deferred — ADR-016 v.1 §2.4/§2.8 phase-2c) keyed in-instance receivers + membroker specificity-routing (routing a follow-up message to a specific running instance) — the conversation-token threading, a follow-up SRD.
- M6 — example + DoD. Inter-instance "A starts/routes to B by key" example (own module); smoke exit 0; coverage gate.
4.7 Tests¶
Persistent waiter (multi-fire, no self-remove, teardown); start-subscription manager (register/unregister; WithManualStart registers no starter); born-from-event instantiation (instance + thresher level — publish a message-start message, assert a new instance is created and completes with the payload bound, mirroring internal/instance/message_flow_test.go + pkg/thresher/thresher_process_test.go); manual-start mode (a WithManualStart process is not auto-instantiated by a published message, and a StartProcess-launched instance waits at its message-start node then completes on delivery); instantiate ReceiveTask; key derivation (composite key from a payload; partial key invalid); resolution (existing-vs-new; two parallel instances correlated by distinct keys; subsequent start joins existing); the example as smoke.
5. Verification (Definition of Done)¶
| # | Check | Expectation |
|---|---|---|
| V1 | Persistent waiter fires repeatedly without self-removing; one-shot in-instance waiter unchanged; clean teardown, -race (FR-1, NFR-2). | green |
| V2 | RegisterProcess (auto, default) registers instance-starters for instantiating start triggers; UnregisterProcess removes them; createTracks no longer parks them in a born-from-event instance. WithManualStart registers no starter and the start node is seeded as a catch in a StartProcess-launched instance (FR-2/3/9). |
green |
| V3 | A published message-start message creates a new instance, born with the start node fired and the payload bound; it runs to completion (FR-3). | green |
| V4 | An instantiate ReceiveTask (WithInstantiate, no incoming) instantiates on a matching message (FR-4). |
green |
| V5 | A composite CorrelationKey is derived from the payload (all properties required); Envelope.CorrelationKey is set by the producer (FR-5/7). |
green |
| V6 | Resolution (phase-2b): two parallel instances are disambiguated by distinct keys; a subsequent start sharing a key joins the existing instance (no duplicate); a no-key message instantiates each time (FR-6). Routing a follow-up message to a specific running instance's keyed receiver is phase-2c (deferred). | green |
| V7 | Inter-instance example (A → B by key) runs to exit 0; existing suites pass (FR-8, NFR-3). | green |
| V8 | make ci green; diff-coverage ≥95 % on touched files; pkg/model imports no internal; held buffer bounded; no goroutine/subscription leak (NFR-2/3/4). |
pass |
6. Risks & regressions¶
- Born-from-event seeding bypasses
checkNodeType. The start node must not also be parked; thecreateTracksskip + the born-from-event track seeding must be consistent, or an instance both spawns and waits. Covered by V2/V3; mirror the §6 trap discipline from SRD-014. - Persistent subscription lifecycle / leak. A never-self-removing waiter must be torn down at
UnregisterProcessand shutdown; otherwise goroutine/subscription leak. NFR-2 + a leak test. - Payload→
Sourceadapter fidelity. AMessagePathevaluating over a reconstructed payload must see the same shape the producer bound; mismatch → wrong/empty key (silent mis-route). Test the round-trip (producer key == consumer-derived key). - Resolution race (new-vs-existing). Two messages for the same not-yet-existing key could each try to instantiate (duplicate instances). The starter must make "create-or-route" atomic per key (single-flight). Covered by the subsequent-start-joins-existing test; guard like the SRD-014 track-step race.
createTrackschange touches all processes. Skipping instantiating starts must not affect none-start or non-message start events; existing thresher/instance suites guard it.- Conversation-less key declaration is an engine choice (§4.5); if it later proves limiting, the Conversation container is the standard escape hatch (deferred).
7. Implementation summary¶
Landed on feat/srd-015-message-correlation-instantiation, one commit per
milestone, each make ci green (build · -race · diff-coverage ≥95% · vuln).
7.1 Milestones by commit¶
| Milestone | Commit | Scope |
|---|---|---|
M1 — hub-owned WaiterFired (unified) |
e49d27f |
singleShot flag on the message waiter; EventHub.WaiterFired; timer waiter migrated off self-removal |
| manual-start mode (doc) | e72a4d7 |
ADR-015/SRD-015 amendment: WithManualStart engine note (FR-9) |
| M2 — instance-starter + manager | 05de56a |
RegisterPersistentEvent + CreatePersistentWaiter; scanInstantiatingStarts; RegisterProcess(…RegisterOption) + WithManualStart; UnregisterProcess; removed dead internal/runner |
| M3 — born-from-event instantiation | 1462886 |
instance.NewFromEvent (variadic born-from-event option); createTracks(bornStart) skip; real launchInstanceFromEvent |
M4 — instantiate ReceiveTask |
8c713d3 |
WithInstantiate; scan matches instantiate ReceiveTasks; snapshot validation accepts an instantiating ReceiveTask as the instantiation point |
| M5a — correlation key derivation | 8e0d9e6 |
NewCorrelationKey/Property/RetrievalExpression; msgflow.DeriveKey + payload→data.Source adapter |
| ADR-016 carve (doc) | 1d2ec63 |
correlation conception split out of ADR-015 → ADR-016; ADR-015 retitled "Event-triggered instantiation" |
| M5b-consumer — key resolution | b509eaf |
events.WithCorrelationKey; starter derives the key; resolveAndLaunch create-or-route-or-join per key |
| M5b-producer — producer key | b79ae08 |
activities.WithCorrelationKey on SendTask; Send stamps Envelope.CorrelationKey |
| M6 — example | e2d6a52 |
examples/inter-instance-correlation/ (split by concern); smoke exit 0 |
| linked-docs sync | 2f127fd |
README capability + examples; SAD-001 §16 ADR catalog (009–016) + §10 instance-creation bullet |
7.2 V-results¶
| Check | Result |
|---|---|
| V1 persistent waiter / teardown / -race | 🟢 |
| V2 starter register-unregister; manual-start registers none | 🟢 |
| V3 born-from-event instance completes, payload bound | 🟢 |
V4 instantiate ReceiveTask |
🟢 |
V5 composite key derived; producer sets Envelope.CorrelationKey |
🟢 |
| V6 disambiguate by key + join (phase-2b); keyed-receiver routing = phase-2c (deferred) | 🟢 (phase-2b) |
| V7 example exits 0; suites pass | 🟢 |
V8 make ci green; diff-coverage ≥95%; no internal import; bounded buffer |
🟢 |
7.3 Notes vs the §4 draft¶
- ADR split (mid-flight). The §4 draft assumed one ADR (ADR-015) for both instantiation and correlation; correlation was carved into the sibling ADR-016 while both were still Draft, so the deferred parts (conversation- token threading, context-based correlation) gained a conceptual home and the SRD's scope phasing (2a/2b/2c) became ADR-decided rather than ad-hoc.
- M5 split. §4.6's "M5 — keyed resolution" was split into M5a (pure derivation), M5b-consumer (starter resolution), M5b-producer (Send sets the key) — each independently testable.
- Born-from-event via a
Newoption, not abuildextraction.Newkeeps its body and gains a born-from-event option (NewFromEventwraps it); this avoids movingNew's defensive error-handling into the diff (a diff-coverage artifact), keeping the unreachable guards out of the changed-line set. internal/runnerremoved. A speculative one-implementation interface with no polymorphic consumer; dropped soRegisterOptionlives natively inthresher(no import-cycle alias).
8. References¶
- ADR-015 v.1 Event-triggered instantiation — the instantiation decision this implements; §2.2 instance-starter, §2.4 entry points, §2.6 deferrals.
- ADR-016 v.1 Message correlation — the correlation decision this implements (phase-2a key derivation done; phase-2b key-based instantiation-resolution); §2.2 key-based, §2.3 resolution model, §2.6 Conversation-less key declaration, §2.7 no-target/bounded, §2.8 phasing.
- ADR-014 v.1 Message Handling — the message tasks/events + the node-agnostic
MessageWaiter+Envelope.CorrelationKeythis builds on. - ADR-006 v.1 Events & Subscriptions — §2.5 the EventHub is the sole owner of waiter removal (no self-removal); SRD-015 adopts it for the message waiter (single-shot removed by the hub, persistent retained) and corrects the current
message.go:250self-removal. - ADR-002 v.1 Extension Architecture — the
MessageBrokerboundary the starter subscribes on; bounded-in-memory defaults. - ADR-001 v.5 Execution Model — instances/tracks/lifecycle the instantiation path feeds.
- SRD-013 v.1 / SRD-014 v.1 — the message tasks/events +
msgflowseam +MessageWaiterreused here (sideways). - BPMN 2.0 §8.4.2 (correlation), §13.2 / §13.5.1 (instantiating start), §13.3.3 (Receive Task) —
docs/bpmn-spec/.
9. Open questions¶
None. Scope is ADR-015 phase-2: event-triggered instantiation (message start event + instantiate ReceiveTask) via a Thresher-hosted instance-starter on a persistent engine subscription, born-from-event seeding with createTracks skipping instantiating starts; and key-based correlation (composite key derived from the payload via CorrelationPropertyRetrievalExpression.MessagePath, Envelope.CorrelationKey, existing-vs-new resolution). Correlation keys are declared at the process level (Conversation out of scope — §4.5 engine note, standard object model preserved). Context-based/predicate correlation, event-based-gateway start, Conversation, durability, and broker-quality guarantees are deferred (§2.2).
Document History¶
| Version | Date | Author | Change |
|---|---|---|---|
| v.1 (Accepted) | 2026-06-16 | Ruslan Gabitov | Accepted at landing — all six milestones (M1–M6) on feat/srd-015-message-correlation-instantiation, each make ci green (build · -race · diff-coverage ≥95% · vuln); the inter-instance example smoke-runs to exit 0. /check-srd PASS (one pre-flip amendment: FR-6/V6 scoped to phase-2b — keyed-receiver routing is the deferred phase-2c). §7 implementation summary filled (milestone SHAs, V-results). |
| v.1 | 2026-06-16 | Ruslan Gabitov | Draft. Implements ADR-015 v.1 phase-2: event-triggered instantiation (message start event + instantiate ReceiveTask) via a Thresher-hosted instance-starter on the existing message waiter given a single-shot/persistent constructor flag, with removal owned by the EventHub (ADR-006 v.1 §2.5 — the waiter never self-removes; the hub removes single-shot waiters after they fire and retains persistent ones, correcting the current message.go:250 self-removal); RegisterProcess registers starters, UnregisterProcess tears them down; createTracks stops seeding instantiating start triggers; a born-from-event instance.NewFromEvent seeds the new instance with the start node pre-fired and the payload bound. Key-based correlation: a composite CorrelationKey derived from the message payload via CorrelationPropertyRetrievalExpression.MessagePath over a payload-backed data.Source (new adapter) through the ExpressionEngine; msgflow.Send sets Envelope.CorrelationKey; resolution routes a message to the existing correlated instance or instantiates a new one (subsequent start sharing the key joins the existing instance). The correlation structs already exist in bpmncommon; this adds builders, a process-level key declaration (Conversation-less engine note — standard object model preserved), and the runtime derivation. Six milestones + an inter-instance "A starts/routes to B by key" example. Deferred: context-based/predicate correlation, event-based-gateway start, Conversation, durability, broker-quality guarantees. Implements ADR-015 v.1. |