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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ use infra_wiring::{Config, ConfigSource, DbPool};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
const EVENT_BUS_CAPACITY: usize = 64;
|
||||
const DEV_JWT_SECRET: &str = "k-template-dev-secret-not-for-production-use-only";
|
||||
|
||||
pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
@@ -29,8 +28,14 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
let auth_service: Arc<dyn domain::ports::AuthService> =
|
||||
Arc::new(adapter_auth::PasswordAuthService);
|
||||
|
||||
let event_bus = Arc::new(adapter_event_publisher::ChannelEventBus::new(EVENT_BUS_CAPACITY));
|
||||
let event_publisher: Arc<dyn domain::ports::EventPublisher> = event_bus.clone();
|
||||
let sqlite_pool = match &pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbPool::Sqlite(p) => p.clone(),
|
||||
};
|
||||
let event_publisher: Arc<dyn domain::ports::EventPublisher> =
|
||||
Arc::new(adapter_event_publisher::SqliteEventPublisher::new(sqlite_pool.clone()));
|
||||
let event_consumer: Arc<dyn domain::ports::EventConsumer> =
|
||||
Arc::new(adapter_event_publisher::SqliteEventConsumer::new(sqlite_pool));
|
||||
|
||||
let provider_registry = build_provider_registry(&config).await;
|
||||
|
||||
@@ -134,10 +139,10 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
bg_event_publisher,
|
||||
));
|
||||
|
||||
let webhook_rx = event_bus.subscriber();
|
||||
let webhook_consumer = event_consumer.clone();
|
||||
let webhook_channel_query = wire_output.channel_query.clone();
|
||||
tokio::spawn(crate::background::webhook_consumer::run(
|
||||
webhook_rx,
|
||||
webhook_consumer,
|
||||
webhook_channel_query,
|
||||
reqwest::Client::new(),
|
||||
));
|
||||
@@ -172,7 +177,8 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
jwt_validator,
|
||||
_library_sync: library_sync,
|
||||
_event_bus: event_bus,
|
||||
_event_publisher: event_publisher,
|
||||
_event_consumer: event_consumer,
|
||||
config: config_arc,
|
||||
_sync_trigger: sync_tx,
|
||||
})
|
||||
|
||||
@@ -36,7 +36,8 @@ pub struct AppState {
|
||||
pub jwt_validator: Option<Arc<adapter_auth::JwtValidator>>,
|
||||
|
||||
pub _library_sync: Arc<dyn domain::ports::LibrarySyncAdapter>,
|
||||
pub _event_bus: Arc<adapter_event_publisher::ChannelEventBus>,
|
||||
pub _event_publisher: Arc<dyn domain::ports::EventPublisher>,
|
||||
pub _event_consumer: Arc<dyn domain::ports::EventConsumer>,
|
||||
|
||||
pub config: Arc<infra_wiring::Config>,
|
||||
pub _sync_trigger: tokio::sync::watch::Sender<()>,
|
||||
|
||||
Reference in New Issue
Block a user