The event envelope, and what can be said about one without touching a broker.
An event crossing a process boundary carries more than its payload: which module emitted it, which request caused it, and which tenant it belongs to. The first is for the reader of a log, the second keeps one correlation id across an asynchronous hop the way the RPC adapter does across a synchronous one (BOU-90), and the third is not optional — an event delivered to the wrong tenant is a data leak, not a bug in ordering.
FC/IS: pure. Nothing here connects to anything.
The event envelope, and what can be said about one without touching a broker. An event crossing a process boundary carries more than its payload: which module emitted it, which request caused it, and which tenant it belongs to. The first is for the reader of a log, the second keeps one correlation id across an asynchronous hop the way the RPC adapter does across a synchronous one (BOU-90), and the third is not optional — an event delivered to the wrong tenant is a data leak, not a bug in ordering. FC/IS: pure. Nothing here connects to anything.
Relay policy for the transactional outbox: when a failing row is dead-lettered, and how long the relay waits after a failed pass (BOU-679).
FC/IS: pure.
Relay policy for the transactional outbox: when a failing row is dead-lettered, and how long the relay waits after a failed pass (BOU-679). FC/IS: pure.
The event bus contract.
The synchronous seam between modules is ports.clj plus, across processes,
the remote-port adapter (BOU-90). This is the asynchronous one: a publisher
does not know who is listening, does not wait, and is not affected if a
consumer is down.
Two protocols rather than one, because publishing and consuming have different lifecycles. A module that only emits events needs no consumer group, no polling thread and no shutdown; making it depend on a protocol full of subscription machinery would be a lie about what it uses.
The event bus contract. The synchronous seam between modules is `ports.clj` plus, across processes, the remote-port adapter (BOU-90). This is the asynchronous one: a publisher does not know who is listening, does not wait, and is not affected if a consumer is down. Two protocols rather than one, because publishing and consuming have different lifecycles. A module that only emits events needs no consumer group, no polling thread and no shutdown; making it depend on a protocol full of subscription machinery would be a lie about what it uses.
An event bus inside one process.
For development, for tests, and for an application that has modules but not yet servers. It implements the same protocols as the Redis adapter so that moving between them is configuration — but it is not a smaller version of it: events do not leave the process, and history dies with it.
Delivery is asynchronous here even though it need not be. A bus that
delivers on the publisher's thread lets a caller depend on the handler
having finished by the time publish! returns, and that assumption then
fails silently against Redis. Matching the weaker guarantee is what makes
the adapters interchangeable.
An event bus inside one process. For development, for tests, and for an application that has modules but not yet servers. It implements the same protocols as the Redis adapter so that moving between them is configuration — but it is not a smaller version of it: events do not leave the process, and history dies with it. Delivery is asynchronous here even though it need not be. A bus that delivers on the publisher's thread lets a caller depend on the handler having finished by the time `publish!` returns, and that assumption then fails silently against Redis. Matching the weaker guarantee is what makes the adapters interchangeable.
An event bus over Redis Streams.
Streams rather than Redis pub/sub, and the difference is the whole reason this is usable. Pub/sub is fire-and-forget: a subscriber that is restarting when an event is published never learns it happened, and there is no way to find out afterwards. A stream keeps its entries, hands each consumer group its own cursor, and tracks what has not been acknowledged — which is what makes at-least-once possible at all.
At-least-once, specifically: an event is delivered until a consumer
acknowledges it, so a consumer that crashes mid-handler sees the event again
when it comes back. Consumers must be idempotent. :id on the envelope is
there for that — it is assigned by the publisher, so it is the same across
redeliveries, unlike the stream entry id.
An event bus over Redis Streams. Streams rather than Redis pub/sub, and the difference is the whole reason this is usable. Pub/sub is fire-and-forget: a subscriber that is restarting when an event is published never learns it happened, and there is no way to find out afterwards. A stream keeps its entries, hands each consumer group its own cursor, and tracks what has not been acknowledged — which is what makes at-least-once possible at all. At-least-once, specifically: an event is delivered until a consumer acknowledges it, so a consumer that crashes mid-handler sees the event again when it comes back. Consumers must be idempotent. `:id` on the envelope is there for that — it is assigned by the publisher, so it is the same across redeliveries, unlike the stream entry id.
Integrant wiring for the event bus.
The layer that emits a key registers it (BOU-131), so an application that
configures :wagoe/events requires this namespace and nothing in the
framework needs to know the module exists.
Integrant wiring for the event bus. The layer that emits a key registers it (BOU-131), so an application that configures `:wagoe/events` requires this namespace and nothing in the framework needs to know the module exists.
Transactional outbox: publish an event as part of a database transaction.
publish-in-tx! writes the event to event_outbox on the caller's open
transaction, so it commits or rolls back with the business change. A relay
polls the table, publishes each pending row to the bus in order, and marks
it delivered. A relay that dies between publish and mark publishes the row
again: delivery is at-least-once, and consumers dedupe on :id (BOU-448).
Relays on several replicas take turns through a lease row in
event_outbox_relay, so one publishes at a time and order holds. The lease
is timed by the database clock, so replica clock skew cannot split it.
A row that fails :max-attempts times over :dead-letter-after-ms, or
cannot be decoded, is dead-lettered and skipped so later events still flow;
after a failed pass the relay backs off (BOU-679). dead-letters lists
them and requeue! puts them back.
Times are epoch-millis BIGINTs and ids text, so the same SQL runs on H2, PostgreSQL, MySQL and SQLite without per-engine encoding.
Transactional outbox: publish an event as part of a database transaction. `publish-in-tx!` writes the event to `event_outbox` on the caller's open transaction, so it commits or rolls back with the business change. A relay polls the table, publishes each pending row to the bus in order, and marks it delivered. A relay that dies between publish and mark publishes the row again: delivery is at-least-once, and consumers dedupe on `:id` (BOU-448). Relays on several replicas take turns through a lease row in `event_outbox_relay`, so one publishes at a time and order holds. The lease is timed by the database clock, so replica clock skew cannot split it. A row that fails `:max-attempts` times over `:dead-letter-after-ms`, or cannot be decoded, is dead-lettered and skipped so later events still flow; after a failed pass the relay backs off (BOU-679). `dead-letters` lists them and `requeue!` puts them back. Times are epoch-millis BIGINTs and ids text, so the same SQL runs on H2, PostgreSQL, MySQL and SQLite without per-engine encoding.
Building and publishing an event, with the parts that are not pure.
wagoe.events.core.event/event takes an id and a timestamp rather than
making them, so it can be tested. This is where they come from — one small
namespace, so that every other caller gets the same envelope instead of each
inventing its own id scheme.
Building and publishing an event, with the parts that are not pure. `wagoe.events.core.event/event` takes an id and a timestamp rather than making them, so it can be tested. This is where they come from — one small namespace, so that every other caller gets the same envelope instead of each inventing its own id scheme.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |