Skip to content

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

  • Publisherdyn-compatible. publish(&[u8], subject: &str) for fire-and-forget; publish_within(&mut tx, ...) for atomic dual-writes (domain row + outbox row in one transaction).
  • Consumerdyn-compatible. next() -> Delivery yields owned Vec<u8>. Consumers call ack() after processing; crash before ack() redelivers on reconnect.
  • Delivery — wrapper over the received bytes + ack handle.
  • NatsPublisher — concrete NATS JetStream impl.
  • nats::outbox_publisher::run — leader-elected drain loop. Reads bus.outbox WHERE published_to_nats = FALSE, publishes to NATS, flips the flag. LISTEN bus_outbox_appended provides 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 patternProducerConsumer / durable
market.klines.binance.{spot|perp}.{symbol_lc}.{tf}gordon-datagordon-bot
intents.executorgordon-botexecutor-default
risk.commandsgordon-riskexecutor-risk
risk.events.{breaker_lowercase}gordon-riskmanager-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. Using Publisher::publish for a dual-write is a correctness bug — the domain commit and the bus insert can diverge.
  • At-least-once. publish_within Ok = durable in bus.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::publish takes &[u8]; Consumer::next yields Delivery. No generic methods. Services pass Box<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)

Gordon — keep compounding without blowing up