use dashmap::DashMap;
use rayon::prelude::*;
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use tokio::sync::mpsc;
use crate::domain::{
DomainResult,
events::{DomainEvent, EventId},
ports::EventPublisherGat,
value_objects::SessionId,
};
use crate::infrastructure::bounded_channel::{ByteBoundedSender, Envelope, byte_bounded_channel};
type NotificationId = u64;
type NotificationCallback = Arc<dyn Fn(&DomainEvent) + Send + Sync>;
const EVENT_CHANNEL_CAPACITY: usize = 1000;
const MAX_QUEUED_EVENT_BYTES: usize = 16 * 1024 * 1024;
const EVENT_LOG_CAPACITY: usize = 10_000;
const EVENT_LOG_EVICT_TARGET: usize = 9_000;
#[cfg(test)]
static EVICTION_RACE_HOOK: std::sync::Mutex<
Option<(std::sync::mpsc::Sender<()>, std::sync::mpsc::Receiver<()>)>,
> = std::sync::Mutex::new(None);
#[cfg(test)]
struct EvictionRacePause {
paused_rx: std::sync::mpsc::Receiver<()>,
resume_tx: std::sync::mpsc::Sender<()>,
}
#[cfg(test)]
impl EvictionRacePause {
fn wait_until_paused(&self) {
self.paused_rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("evict_oldest_if_over_capacity never reached the armed pause point");
}
fn resume(self) {
let _ = self.resume_tx.send(());
}
}
#[cfg(test)]
fn arm_eviction_race_pause() -> EvictionRacePause {
let (paused_tx, paused_rx) = std::sync::mpsc::channel();
let (resume_tx, resume_rx) = std::sync::mpsc::channel();
*EVICTION_RACE_HOOK.lock().unwrap() = Some((paused_tx, resume_rx));
EvictionRacePause {
paused_rx,
resume_tx,
}
}
pub struct InMemoryEventPublisher {
notification_callbacks: Arc<DashMap<NotificationId, NotificationCallback>>,
event_log: Arc<DashMap<EventId, StoredEvent>>,
next_notification_id: Arc<AtomicU64>,
next_sequence: Arc<AtomicU64>,
channel_tx: Arc<tokio::sync::RwLock<Option<ByteBoundedSender<StoredEvent>>>>,
}
impl Clone for InMemoryEventPublisher {
fn clone(&self) -> Self {
Self {
notification_callbacks: Arc::clone(&self.notification_callbacks),
event_log: Arc::clone(&self.event_log),
next_notification_id: Arc::clone(&self.next_notification_id),
next_sequence: Arc::clone(&self.next_sequence),
channel_tx: Arc::clone(&self.channel_tx),
}
}
}
#[derive(Debug, Clone)]
pub struct StoredEvent {
pub id: EventId,
pub event_type: String,
pub session_id: Option<SessionId>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub metadata: std::collections::HashMap<String, String>,
pub sequence: u64,
}
impl StoredEvent {
fn approx_byte_size(&self) -> usize {
const FIXED_FIELD_ALLOWANCE: usize = 96;
self.event_type.len()
+ self
.metadata
.iter()
.map(|(k, v)| k.len() + v.len())
.sum::<usize>()
+ FIXED_FIELD_ALLOWANCE
}
}
impl InMemoryEventPublisher {
pub fn new() -> Self {
Self {
notification_callbacks: Arc::new(DashMap::new()),
event_log: Arc::new(DashMap::new()),
next_notification_id: Arc::new(AtomicU64::new(1)),
next_sequence: Arc::new(AtomicU64::new(0)),
channel_tx: Arc::new(tokio::sync::RwLock::new(None)),
}
}
pub fn with_channel() -> (Self, mpsc::Receiver<Envelope<StoredEvent>>) {
let (tx, rx) = byte_bounded_channel(EVENT_CHANNEL_CAPACITY, MAX_QUEUED_EVENT_BYTES);
let publisher = Self {
notification_callbacks: Arc::new(DashMap::new()),
event_log: Arc::new(DashMap::new()),
next_notification_id: Arc::new(AtomicU64::new(1)),
next_sequence: Arc::new(AtomicU64::new(0)),
channel_tx: Arc::new(tokio::sync::RwLock::new(Some(tx))),
};
(publisher, rx)
}
pub fn add_notification_callback<F>(&self, callback: F) -> NotificationId
where
F: Fn(&DomainEvent) + Send + Sync + 'static,
{
let id = self.next_notification_id.fetch_add(1, Ordering::Relaxed);
self.notification_callbacks.insert(id, Arc::new(callback));
id
}
pub fn remove_notification_callback(&self, id: NotificationId) -> Option<NotificationCallback> {
self.notification_callbacks
.remove(&id)
.map(|(_, callback)| callback)
}
pub fn event_count(&self) -> usize {
self.event_log.len()
}
pub fn events_by_type(&self, event_type: &str) -> Vec<StoredEvent> {
self.event_log
.iter()
.filter(|entry| entry.value().event_type == event_type)
.map(|entry| entry.value().clone())
.collect()
}
pub fn events_for_session(&self, session_id: SessionId) -> Vec<StoredEvent> {
self.event_log
.iter()
.filter(|entry| entry.value().session_id == Some(session_id))
.map(|entry| entry.value().clone())
.collect()
}
pub fn clear(&self) {
self.event_log.clear();
}
fn evict_oldest_if_over_capacity(&self) {
if self.event_log.len() > EVENT_LOG_CAPACITY {
#[cfg(test)]
let armed_pause = EVICTION_RACE_HOOK.lock().unwrap().take();
#[cfg(test)]
if let Some((paused_tx, resume_rx)) = armed_pause {
let _ = paused_tx.send(());
let _ = resume_rx.recv();
}
let mut by_sequence: Vec<(EventId, u64)> = self
.event_log
.iter()
.map(|entry| (*entry.key(), entry.value().sequence))
.collect();
by_sequence.sort_unstable_by_key(|(_, sequence)| *sequence);
let excess = by_sequence.len().saturating_sub(EVENT_LOG_EVICT_TARGET);
for (key, _) in by_sequence.into_iter().take(excess) {
self.event_log.remove(&key);
}
}
}
pub fn recent_events(&self, limit: usize) -> Vec<StoredEvent> {
let mut events: Vec<StoredEvent> = self
.event_log
.iter()
.map(|entry| entry.value().clone())
.collect();
events.sort_unstable_by_key(|event| std::cmp::Reverse(event.sequence));
events.truncate(limit);
events
}
}
impl std::fmt::Debug for InMemoryEventPublisher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InMemoryEventPublisher")
.field("async_fields", &"<async RwLock>")
.finish()
}
}
impl Default for InMemoryEventPublisher {
fn default() -> Self {
Self::new()
}
}
impl EventPublisherGat for InMemoryEventPublisher {
type PublishFuture<'a>
= impl std::future::Future<Output = DomainResult<()>> + Send + 'a
where
Self: 'a;
type PublishBatchFuture<'a>
= impl std::future::Future<Output = DomainResult<()>> + Send + 'a
where
Self: 'a;
fn publish(&self, event: DomainEvent) -> Self::PublishFuture<'_> {
async move {
let stored_event = StoredEvent {
id: EventId::new(),
event_type: event.event_type().to_string(),
session_id: Some(event.session_id()),
timestamp: event.occurred_at(),
metadata: event.metadata(),
sequence: self.next_sequence.fetch_add(1, Ordering::Relaxed),
};
let event_id = stored_event.id;
self.event_log.insert(event_id, stored_event.clone());
self.evict_oldest_if_over_capacity();
let approx_size = stored_event.approx_byte_size();
if let Some(tx) = self.channel_tx.read().await.as_ref()
&& let Err(e) = tx.try_send(stored_event, approx_size)
{
tracing::warn!("Dropping event from streaming channel: {e:?}");
}
self.notification_callbacks.iter().for_each(|entry| {
let callback = entry.value();
callback(&event);
});
Ok(())
}
}
fn publish_batch(&self, events: Vec<DomainEvent>) -> Self::PublishBatchFuture<'_> {
async move {
let base_sequence = self
.next_sequence
.fetch_add(events.len() as u64, Ordering::Relaxed);
let stored_events: Vec<_> = events
.into_par_iter()
.enumerate()
.map(|(i, event)| {
let stored_event = StoredEvent {
id: EventId::new(),
event_type: event.event_type().to_string(),
session_id: Some(event.session_id()),
timestamp: event.occurred_at(),
metadata: event.metadata(),
sequence: base_sequence + i as u64,
};
let event_id = stored_event.id;
self.event_log.insert(event_id, stored_event.clone());
self.notification_callbacks.iter().for_each(|entry| {
let callback = entry.value();
callback(&event);
});
stored_event
})
.collect();
if let Some(tx) = self.channel_tx.read().await.as_ref() {
for stored_event in stored_events {
let approx_size = stored_event.approx_byte_size();
if let Err(e) = tx.try_send(stored_event, approx_size) {
tracing::warn!("Dropping event from streaming channel: {e:?}");
}
}
}
self.evict_oldest_if_over_capacity();
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{
events::DomainEvent,
value_objects::{SessionId, StreamId},
};
#[tokio::test]
async fn test_in_memory_event_publisher() {
let publisher = InMemoryEventPublisher::new();
let received_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let events_clone = received_events.clone();
publisher.add_notification_callback(move |event| {
events_clone.lock().unwrap().push(event.clone());
});
let session_id = SessionId::new();
let event = DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
};
publisher.publish(event).await.unwrap();
assert_eq!(publisher.event_count(), 1);
assert_eq!(received_events.lock().unwrap().len(), 1);
let events_for_session = publisher.events_for_session(session_id);
assert_eq!(events_for_session.len(), 1);
}
#[tokio::test]
async fn test_event_publisher_with_channel() {
let (publisher, mut rx) = InMemoryEventPublisher::with_channel();
let session_id = SessionId::new();
let event = DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
};
publisher.publish(event).await.unwrap();
let received = rx.recv().await.unwrap();
assert_eq!(received.event_type, "session_activated");
assert_eq!(received.session_id, Some(session_id));
}
#[tokio::test]
async fn test_batch_publishing() {
let publisher = InMemoryEventPublisher::new();
let session_id = SessionId::new();
let stream_id = StreamId::new();
let events = vec![
DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
},
DomainEvent::StreamStarted {
session_id,
stream_id,
timestamp: chrono::Utc::now(),
},
];
publisher.publish_batch(events).await.unwrap();
assert_eq!(publisher.event_count(), 2);
}
#[tokio::test]
async fn test_structurally_identical_events_do_not_collide() {
let publisher = InMemoryEventPublisher::new();
let session_id = SessionId::new();
let timestamp = chrono::Utc::now();
let event1 = DomainEvent::SessionActivated {
session_id,
timestamp,
};
let event2 = DomainEvent::SessionActivated {
session_id,
timestamp,
};
assert_eq!(
format!("{event1:?}"),
format!("{event2:?}"),
"events must be structurally identical for this regression test to be meaningful"
);
publisher.publish(event1).await.unwrap();
publisher.publish(event2).await.unwrap();
assert_eq!(
publisher.event_count(),
2,
"both events must be stored under distinct EventIds, not overwrite each other"
);
let stored = publisher.events_for_session(session_id);
assert_eq!(stored.len(), 2);
assert_ne!(
stored[0].id, stored[1].id,
"structurally identical events must still get distinct EventIds"
);
}
#[tokio::test]
async fn test_streaming_channel_is_bounded() {
let (publisher, _rx) = InMemoryEventPublisher::with_channel();
let tx = publisher
.channel_tx
.read()
.await
.clone()
.expect("with_channel must configure a sender");
assert_eq!(tx.max_capacity(), EVENT_CHANNEL_CAPACITY);
}
#[tokio::test]
async fn test_publish_does_not_block_when_streaming_channel_is_full() {
let (publisher, mut rx) = InMemoryEventPublisher::with_channel();
let session_id = SessionId::new();
for _ in 0..EVENT_CHANNEL_CAPACITY {
let event = DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
};
publisher.publish(event).await.unwrap();
}
assert_eq!(
publisher
.channel_tx
.read()
.await
.as_ref()
.expect("with_channel must configure a sender")
.capacity(),
0,
"channel should be at capacity after publishing EVENT_CHANNEL_CAPACITY events"
);
let event = DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
};
tokio::time::timeout(std::time::Duration::from_secs(2), publisher.publish(event))
.await
.expect("publish must not block when the streaming channel is full")
.unwrap();
assert_eq!(publisher.event_count(), EVENT_CHANNEL_CAPACITY + 1);
rx.close();
let mut drained = 0;
while rx.try_recv().is_ok() {
drained += 1;
}
assert_eq!(
drained, EVENT_CHANNEL_CAPACITY,
"the overflow event must have been dropped from the channel, not queued"
);
}
#[tokio::test]
async fn test_streaming_channel_rejects_event_exceeding_byte_budget() {
let (tx, _rx) = byte_bounded_channel::<StoredEvent>(EVENT_CHANNEL_CAPACITY, 10);
let mut large_metadata = std::collections::HashMap::new();
large_metadata.insert("key".to_string(), "x".repeat(1000));
let event = StoredEvent {
id: EventId::new(),
event_type: "test".to_string(),
session_id: None,
timestamp: chrono::Utc::now(),
metadata: large_metadata,
sequence: 0,
};
let size = event.approx_byte_size();
assert!(
matches!(
tx.try_send(event, size),
Err(crate::infrastructure::bounded_channel::TrySendError::BudgetExceeded(_))
),
"an event whose approximate size exceeds the byte budget must be rejected"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_eviction_forced_interleaving_stays_at_target() {
let publisher = Arc::new(InMemoryEventPublisher::new());
let session_id = SessionId::new();
let pause = arm_eviction_race_pause();
let publisher_a = Arc::clone(&publisher);
let task_a = tokio::spawn(async move {
let events: Vec<DomainEvent> = (0..20_000)
.map(|_| DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
})
.collect();
publisher_a.publish_batch(events).await.unwrap();
});
let paused = tokio::task::spawn_blocking(move || {
pause.wait_until_paused();
pause
})
.await
.unwrap();
publisher
.publish(DomainEvent::SessionActivated {
session_id,
timestamp: chrono::Utc::now(),
})
.await
.unwrap();
paused.resume();
task_a.await.unwrap();
assert_eq!(
publisher.event_count(),
EVENT_LOG_EVICT_TARGET,
"forced eviction-race interleaving left event_log away from the deterministic target"
);
}
}