gordon-bus
Purpose
Event-bus client library for the Gordon platform. Provides Publisher and Consumer traits backed by NATS JetStream, with a Postgres outbox for atomic dual-writes and a leader-elected drain process. The crate carries the transport mechanics; the payload types live in gordon-protocol.
Version + status
2.1.0 — production. Single NATS JetStream backend over the Postgres outbox. The Wave 3 cutover (2026-05-08/09) put NATS on the hot path. 2.0 (2026-05-15) dropped the no-longer-used pg-LISTEN consumer code path. Publisher / Consumer surface wire-stable.
Public surface
Publisher—dyn-compatible.publish(&[u8], subject: &str)for fire-and-forget;publish_within(&mut tx, ...)for atomic dual-writes (domain row + outbox row in one transaction).Consumer—dyn-compatible.next() -> Deliveryyields ownedVec<u8>. Consumers callack()after processing; crash beforeack()redelivers on reconnect.Delivery— wrapper over the received bytes + ack handle.NatsPublisher— concrete NATS JetStream impl.nats::outbox_publisher::run— leader-elected drain loop. Readsbus.outbox WHERE published_to_nats = FALSE, publishes to NATS, flips the flag.LISTEN bus_outbox_appendedprovides sub-100 ms wakeup; idle poll fallback every 1 s.
NATS stream configuration
Single JetStream stream gordon-bus:
- Retention: 168 h (7 days)
- Size cap: 768 MiB
- 5 subject wildcards:
| Subject pattern | Producer | Consumer / durable |
|---|---|---|
market.klines.binance.{spot|perp}.{symbol_lc}.{tf} | gordon-data | gordon-bot |
intents.executor | gordon-bot | executor-default |
risk.commands | gordon-risk | executor-risk |
risk.events.{breaker_lowercase} | gordon-risk | manager-risk |
trading.fills.{bot_id} | gordon-executor | (TBD) |
Outbox-first publish path
bus.outbox is the system-of-record. Producers INSERT a row atomically with their domain transaction via publish_within; the leader-elected drain forwards to NATS.
Writers never wait on NATS. NATS never becomes the source of truth.
- Outbox atomicity. Dual-writes (domain row + bus event) MUST use
publish_within. UsingPublisher::publishfor a dual-write is a correctness bug — the domain commit and the bus insert can diverge. - At-least-once.
publish_withinOk = durable inbus.outbox. Consumers MUST be idempotent (canonical:intent_id UUID-v7+ON CONFLICT DO NOTHING). - Single leader.
pg_try_advisory_lock(OUTBOX_PUBLISHER_LOCK_ID). Lock auto-releases on connection close — no zombie-leader window.
Dependencies of note
sqlx— outbox table reads / writes and advisory lock.async-nats— NATS JetStream client.- No runtime gordon-* deps. Bus types live in gordon-protocol; gordon-bus does not depend on gordon-protocol. Consumers wire both deps side-by-side.
Invariants
- Trait surface is
dyn-compatible.Publisher::publishtakes&[u8];Consumer::nextyieldsDelivery. No generic methods. Services passBox<dyn Publisher>. - Codec at the call site. Encode / decode helpers live with the canonical types in gordon-protocol. The bus carries bytes.
- No runtime gordon-* deps. gordon-bus is upstream of every consumer; pulling gordon-protocol as a dep would close a dependency cycle.
- Zero
#[allow(...)]. - Module size cap: 1000 lines. Pre-commit hook rejects; warns at 500.
Where it lives
- Repo:
gordon-bus/ - kellnr:
https://kellnr.lepaux.com/crates/gordon-bus(LAN-only)
Related
- gordon-protocol — payload types ferried by this transport.
- gordon-migrate — owns
bus.outboxschema (migrations 0017 + 0025). - Architecture: Event Bus Topology