feat: worker relays outbox to nats

This commit is contained in:
2026-10-26 00:06:09 +01:00
parent 32dfc84697
commit c06d3d9207
15 changed files with 678 additions and 102 deletions

View File

@@ -0,0 +1,48 @@
//! Moves outbox rows onto the message bus. The worker runs it when the bus is
//! not the outbox itself (`EVENT_BUS_BACKEND=nats`).
//!
//! Rows are claimed by the SQLite queue's own atomic claim, so a row is only
//! ever relayed by one claimant and failures take the queue's existing
//! backoff and dead-letter path. A row is marked done only after the sink
//! reports the bus accepted it; a crash in between re-relays the row, and the
//! bus de-duplicates on the event id.
use std::sync::Arc;
use domain::{
events::EventEnvelope,
ports::{EventConsumer, EventRelaySink},
};
use futures::StreamExt;
use tokio::task::JoinHandle;
pub fn spawn(source: Arc<dyn EventConsumer>, sink: Arc<dyn EventRelaySink>) -> JoinHandle<()> {
tokio::spawn(async move {
let mut rows = source.consume();
while let Some(next) = rows.next().await {
match next {
Ok(envelope) => relay_envelope(envelope, sink.as_ref()).await,
// A row that cannot even be decoded stays claimed; there is
// no handle to settle it with.
Err(e) => tracing::error!("outbox relay: undecodable row: {e}"),
}
}
tracing::warn!("outbox relay: source closed, relay stopped");
})
}
async fn relay_envelope(envelope: EventEnvelope, sink: &dyn EventRelaySink) {
match sink.relay(envelope.id(), &envelope.event).await {
Ok(()) => {
if let Err(e) = envelope.ack().await {
tracing::error!("outbox relay: published but could not mark done: {e}");
}
}
Err(e) => {
tracing::warn!("outbox relay: publish failed, will retry: {e}");
if let Err(e) = envelope.nack().await {
tracing::error!("outbox relay: could not nack: {e}");
}
}
}
}