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:
@@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::value_objects::{ChannelId, ScheduleId, SlotId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum DomainEvent {
|
||||
BroadcastTransition {
|
||||
@@ -18,6 +20,58 @@ pub enum DomainEvent {
|
||||
UserRegistered { user_id: crate::value_objects::UserId },
|
||||
}
|
||||
|
||||
pub struct EventEnvelope {
|
||||
id: i64,
|
||||
event: DomainEvent,
|
||||
retry_count: u32,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
impl EventEnvelope {
|
||||
pub fn new(id: i64, event: DomainEvent) -> Self {
|
||||
Self {
|
||||
id,
|
||||
event,
|
||||
retry_count: 0,
|
||||
created_at: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_persistence(
|
||||
id: i64,
|
||||
event: DomainEvent,
|
||||
retry_count: u32,
|
||||
created_at: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
event,
|
||||
retry_count,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> i64 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn event(&self) -> &DomainEvent {
|
||||
&self.event
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> DomainEvent {
|
||||
self.event
|
||||
}
|
||||
|
||||
pub fn retry_count(&self) -> u32 {
|
||||
self.retry_count
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> &str {
|
||||
&self.created_at
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/mod.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
pub use crate::events::DomainEvent;
|
||||
pub use crate::events::{DomainEvent, EventEnvelope};
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventPublisher: Send + Sync {
|
||||
@@ -10,7 +10,9 @@ pub trait EventPublisher: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventConsumer: Send + Sync {
|
||||
async fn recv(&self) -> DomainResult<DomainEvent>;
|
||||
async fn poll_next(&self) -> DomainResult<Option<EventEnvelope>>;
|
||||
async fn ack(&self, event_id: i64) -> DomainResult<()>;
|
||||
async fn nack(&self, event_id: i64, error: &str) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -13,7 +13,7 @@ pub mod user;
|
||||
pub use activity::{ActivityLogCommand, ActivityLogQuery};
|
||||
pub use auth::{AuthService, TokenService};
|
||||
pub use channel::{ChannelCommand, ChannelQuery};
|
||||
pub use events::{DomainEvent, EventConsumer, EventHandler, EventPublisher};
|
||||
pub use events::{DomainEvent, EventConsumer, EventEnvelope, EventHandler, EventPublisher};
|
||||
pub use library::{LibraryCommand, LibraryQuery, LibrarySyncAdapter};
|
||||
pub use media::{
|
||||
Collection, IMediaProvider, IProviderRegistry, ProviderCapabilities, SeriesSummary,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
use crate::events::{DomainEvent, EventEnvelope};
|
||||
use crate::models::{
|
||||
ActivityEvent, Channel, ChannelConfigSnapshot, GeneratedSchedule, LibraryCollection,
|
||||
LibrarySyncLogEntry, LibrarySyncResult, MediaItem, PlaybackRecord, ProviderConfigRow,
|
||||
@@ -13,8 +14,9 @@ use crate::models::{
|
||||
};
|
||||
use crate::ports::{
|
||||
ActivityLogCommand, ActivityLogQuery, AppSettingsRepository, ChannelCommand, ChannelQuery,
|
||||
LibraryCommand, LibraryQuery, ProviderConfigCommand, ProviderConfigQuery, ScheduleCommand,
|
||||
ScheduleQuery, TranscodeSettingsRepository, UserCommand, UserQuery,
|
||||
EventConsumer, EventPublisher, LibraryCommand, LibraryQuery, ProviderConfigCommand,
|
||||
ProviderConfigQuery, ScheduleCommand, ScheduleQuery, TranscodeSettingsRepository, UserCommand,
|
||||
UserQuery,
|
||||
};
|
||||
use crate::value_objects::{
|
||||
BlockId, ChannelId, ContentType, LibrarySearchFilter, MediaItemId, ScheduleId, SnapshotId,
|
||||
@@ -777,3 +779,63 @@ impl TranscodeSettingsRepository for InMemoryTranscodeSettings {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InMemoryEventBus {
|
||||
queue: Mutex<VecDeque<(i64, DomainEvent)>>,
|
||||
next_id: Mutex<i64>,
|
||||
}
|
||||
|
||||
impl InMemoryEventBus {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
next_id: Mutex::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn events(&self) -> Vec<DomainEvent> {
|
||||
self.queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryEventBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventPublisher for InMemoryEventBus {
|
||||
async fn publish(&self, event: DomainEvent) -> DomainResult<()> {
|
||||
let mut next = self.next_id.lock().unwrap();
|
||||
let id = *next;
|
||||
*next += 1;
|
||||
self.queue.lock().unwrap().push_back((id, event));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for InMemoryEventBus {
|
||||
async fn poll_next(&self) -> DomainResult<Option<EventEnvelope>> {
|
||||
let front = self.queue.lock().unwrap().front().cloned();
|
||||
Ok(front.map(|(id, event)| EventEnvelope::new(id, event)))
|
||||
}
|
||||
|
||||
async fn ack(&self, event_id: i64) -> DomainResult<()> {
|
||||
self.queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.retain(|(id, _)| *id != event_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn nack(&self, _event_id: i64, _error: &str) -> DomainResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
use crate::events::DomainEvent;
|
||||
use crate::events::{DomainEvent, EventEnvelope};
|
||||
use crate::models::{
|
||||
ActivityEvent, LibrarySyncResult, MediaItem,
|
||||
};
|
||||
use crate::ports::{
|
||||
ActivityLogCommand, ActivityLogQuery, EventPublisher, IMediaProvider, LibrarySyncAdapter,
|
||||
ProviderCapabilities, StreamQuality, StreamingProtocol,
|
||||
ActivityLogCommand, ActivityLogQuery, EventConsumer, EventPublisher, IMediaProvider,
|
||||
LibrarySyncAdapter, ProviderCapabilities, StreamQuality, StreamingProtocol,
|
||||
};
|
||||
use crate::value_objects::{ChannelId, MediaFilter, MediaItemId};
|
||||
|
||||
@@ -32,6 +32,35 @@ impl EventPublisher for NoopEventPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoopEventConsumer;
|
||||
|
||||
impl NoopEventConsumer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NoopEventConsumer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for NoopEventConsumer {
|
||||
async fn poll_next(&self) -> DomainResult<Option<EventEnvelope>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn ack(&self, _event_id: i64) -> DomainResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn nack(&self, _event_id: i64, _error: &str) -> DomainResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoopMediaProvider;
|
||||
|
||||
impl NoopMediaProvider {
|
||||
|
||||
Reference in New Issue
Block a user