Eventing Pipeline
Table of Contents
1. Overview
Every entity change in ORE Studio flows through a four-stage pipeline:
- PostgreSQL fires a
pg_notifyon a named channel. - The service's
postgres_event_sourcedeserialises the notification and publishes the entity's typed event, and anentity_change_eventthat carries the tenant, to the in-processevent_bus. - A subscriber in the service publishes the typed event to NATS on the
subject its
entity_event_traitsstate. - A client (or any NATS subscriber) receives the NATS message and reacts.
The diagram below shows these stages for a workunit change:
┌─────────────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ ores_compute_workunits_tbl → ores_compute_workunits_notify_trg │
│ → pg_notify('ores_compute_workunits', {event_id, entity, key, │
│ action, version, occurred_at, correlation_id, tenant_id}) │
└───────────────────────────┬─────────────────────────────────────────┘
│ LISTEN/NOTIFY (libpq)
┌───────────────────────────▼─────────────────────────────────────────┐
│ ores.compute.service (server process) │
│ postgres_event_source → event_bus │
│ workunit_event, entity_change_event │
│ → subscriber → nats.publish("compute.v1.events.workunits.updated") │
└───────────────────────────┬─────────────────────────────────────────┘
│ NATS publish/subscribe
┌───────────────────────────▼─────────────────────────────────────────┐
│ NATS subscriber (another service, or the web server's event stream) │
│ subscribe("compute.v1.workunits_events.>") │
└─────────────────────────────────────────────────────────────────────┘
2. Stage 1 — PostgreSQL pg_notify
Every table that participates in eventing has a dedicated AFTER trigger
that calls pg_notify. The trigger is generated into
ores.sql; the workunit example is in
projects/ores.sql/create/compute/compute_workunits_notify_trigger_create.sql.
The notification payload is a JSON object, the specification's event fields plus the tenant:
| Field | Type | Meaning |
|---|---|---|
event_id |
text |
Identifies this publication; a redelivery carries the same id |
entity |
text |
The entity's fully-qualified name, such as ores.compute.workunit |
key |
text |
The changed row's key, as a JSON object such as {"id": "..."} |
action |
text |
created, updated or deleted |
version |
int |
The version the change produced |
occurred_at |
text |
UTC time of the change |
correlation_id |
text |
The request that caused the change, when one is known |
tenant_id |
text |
The tenant that owns the row; published in the X-Tenant-Id header, not in the payload |
party_id |
text |
The party that owns the row, on a party-owned table only; published in the X-Party-Id header |
The channel name (first argument to pg_notify) follows the convention
ores_<table_base_name>, e.g. ores_compute_workunits for
ores_compute_workunits_tbl. A versioned table's update closes the current
row and the insert that follows carries the change, so the close announces
nothing.
2.1. Naming conventions
An entity's change event is addressed by three subjects, one per action:
{component}.v1.{resource}_events.{action}
Examples:
refdata.v1.events.currencies.updatedcompute.v1.events.workunits.creatediam.v1.events.accounts.deleted
The prefix, refdata.v1.currencies_events, is stated once in the event's
entity_event_traits. A subscriber that wants one action subscribes to one
subject. A subscriber that wants all three asks event_subject_wildcard<T>()
for <prefix>.>; the bare prefix matches none of them.
3. Stage 2 — Server-side pickup (postgres_event_source → event_bus)
postgres_event_source (in ores.eventing) maintains a persistent
PostgreSQL LISTEN connection. On each incoming notification it:
- Deserialises the JSON payload into an
entity_event_notification. - Finds the mapping registered for the channel and converts the
notification into the entity's typed event, through the event's own
entity_event_traits::from_notification, which knows the entity's key. - Publishes the typed event on the in-process
event_bus. - Publishes an
entity_change_event– the entity's name, the changed ids and the tenant – on the same bus. - Publishes a
published_entity_event<Event>– the typed event with the tenant and the party the store reported – on the same bus. The generated registrar subscribes to this one and publishes to NATS.
Mappings are registered during application startup. The generated per-entity registrar does it:
event_source.register_entity_event_mapping<compute::messaging::workunit_event>( "ores_compute_workunits");
The argument is the PostgreSQL channel name; it must match pg_notify.
3.1. When a subscriber needs the tenant
The typed event states the row, the action and the version. It does not state
the tenant. A subscriber that has to open a tenant context, such as the
compute dispatcher, subscribes to entity_change_event and keeps the entity it
serves:
event_bus.subscribe<ores::eventing::domain::entity_change_event>( [&dispatcher](const ores::eventing::domain::entity_change_event& e) { if (e.entity == "ores.compute.workunit") dispatcher.dispatch(e); });
A service that listens on another component's table maps the channel to that component's event, as the marketdata service does for the CRM tables, and publishes nothing: the owning service publishes.
4. Stage 3 — NATS publish
The generated registrar also subscribes to the typed event and publishes it to NATS on the subject the action names:
return event_bus.subscribe<compute::messaging::workunit_event>( [&nats](const compute::messaging::workunit_event& e) { ev::service::publish_entity_event( nats, ev::domain::event_subject<compute::messaging::workunit_event>(e.action), e); });
4.1. Events that are not entity changes
A domain event that is not a change to an entity, such as a role being
assigned, is a hand-written type with an event_traits specialisation. Its
name identifies it on the in-process bus, and it is not published to NATS.
ores.iam declares role_assigned_event, role_revoked_event and
account_permissions_changed_event this way.
5. Stage 4 — Client pickup
A NATS subscriber registers interest in a prefix's wildcard and receives the
typed event as the payload. The tenant and the party are in the envelope, as
X-Tenant-Id and X-Party-Id; the protocol specification defines them (Events,
Tenancy of an event). The web server holds one subscription per entity for all the
sessions watching it and relays the events to each browser on GET /api/events; see
Change notification. The registry listens on the
wildcard of each collection's prefix, such as iam.v1.accounts_events.>, and
relays the time of the change and the key of the record, which the typed event
carries in its key field. It joins close events into one message and names up
to 20 records; see Change notification. It tells a session only of an event whose tenant
is the session's own or the system tenant's, and, for a member, whose party the
member works in. An event with no tenant header is told to nobody. The broker
does not enforce this, so the rule is kept by the subscriber.
6. Verifying eventing
The events commands of ores.shell print "Event subscriptions are not
available in NATS mode", so they cannot show an event. To watch one, subscribe
to the wildcard with the NATS command line client, under the environment's
subject prefix (the services log it at startup), and write to the entity by
any path:
nats sub 'ores.dev.<environment>.compute.v1.workunits_events.>'
If nothing arrives within a second, check:
- The SQL trigger exists:
\df ores_compute_workunits_notify_fninpsql. - The service started its event source, and the log names the channel.
- The subject matches exactly; subjects are case-sensitive.
7. Adding eventing to a new entity
Eventing is generated. The nats-eventing and nats-event-registrar facets
of the entity's model produce:
- SQL trigger —
<table>_notify_trigger_create.sql, callingpg_notifyafter insert, update and delete. - Event traits —
<domain>.api/include/<domain>.api/eventing/<entity>_event.hppwith theentity_event_traitsspecialisation that states the subject prefix. The event struct sits beside the entity's key in its protocol header. - Registrar —
register_<entity>_event_mapping(), which maps the channel to the event and publishes it to NATS.
The service's application.cpp calls the registrar before
event_source.start(), and a registrar that is not called publishes nothing.
Add ores.eventing.core to the service CMakeLists.
8. See also
- ores.eventing — component overview
- ores.sql — database schema and triggers
- Change notification — how the web interface consumes these events