replace tokio::broadcast event bus with SQLite-backed event queue (ADR-0003)

- DomainEvent: add Serialize/Deserialize
- EventConsumer port: poll_next/ack/nack replacing recv()
- EventEnvelope: wraps event with id/retry_count/created_at
- SqliteEventPublisher/SqliteEventConsumer: INSERT/poll/DLQ
- migration: event_queue + dead_letter_queue tables
- InMemoryEventBus: combined publisher+consumer test double
- NoopEventConsumer: test stub
- webhook_consumer: polls EventConsumer instead of broadcast::Receiver
- presentation+mcp wiring: create from SqlitePool
This commit is contained in:
2026-07-12 07:23:21 +02:00
parent e2393be635
commit 826e824b58
13 changed files with 380 additions and 55 deletions

View File

@@ -3,24 +3,26 @@ use std::sync::Arc;
use chrono::Utc;
use handlebars::Handlebars;
use serde_json::{Value, json};
use tokio::sync::broadcast;
use uuid::Uuid;
use domain::events::DomainEvent;
use domain::ports::ChannelQuery;
use domain::ports::{ChannelQuery, EventConsumer};
const DEFAULT_CONTENT_TYPE: &str = "application/json";
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
pub async fn run(
mut rx: broadcast::Receiver<DomainEvent>,
consumer: Arc<dyn EventConsumer>,
channel_query: Arc<dyn ChannelQuery>,
client: reqwest::Client,
) {
loop {
match rx.recv().await {
Ok(event) => {
let channel_id = event_channel_id(&event);
let payload = build_payload(&event);
match consumer.poll_next().await {
Ok(Some(envelope)) => {
let event_id = envelope.id();
let event = envelope.event();
let channel_id = event_channel_id(event);
let payload = build_payload(event);
let channel_id_vo = domain::ChannelId::from(channel_id);
match channel_query.find_by_id(channel_id_vo).await {
@@ -51,13 +53,17 @@ pub async fn run(
);
}
}
if let Err(e) = consumer.ack(event_id).await {
tracing::warn!("webhook consumer: ack failed for event {}: {}", event_id, e);
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!("webhook consumer lagged, {} events dropped", n);
Ok(None) => {
tokio::time::sleep(POLL_INTERVAL).await;
}
Err(broadcast::error::RecvError::Closed) => {
tracing::info!("webhook consumer: event bus closed, shutting down");
break;
Err(e) => {
tracing::warn!("webhook consumer: poll error: {}", e);
tokio::time::sleep(POLL_INTERVAL).await;
}
}
}