use std::time::SystemTime;
use tokio::sync::broadcast;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum JobEventType {
Queued,
Started,
Completed,
Failed,
Cancelled,
}
impl JobEventType {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Queued => "queued",
Self::Started => "started",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
}
}
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
}
}
impl std::fmt::Display for JobEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct JobEvent {
pub job_id: Uuid,
pub event_type: JobEventType,
#[serde(with = "system_time_serde")]
pub timestamp: SystemTime,
}
impl JobEvent {
#[must_use]
pub fn new(job_id: Uuid, event_type: JobEventType) -> Self {
Self {
job_id,
event_type,
timestamp: SystemTime::now(),
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.event_type.is_terminal()
}
}
mod system_time_serde {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
let epoch_secs = t
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
s.serialize_u64(epoch_secs)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
let secs = u64::deserialize(d)?;
Ok(UNIX_EPOCH + Duration::from_secs(secs))
}
}
pub struct NotificationBus {
tx: broadcast::Sender<JobEvent>,
}
impl NotificationBus {
#[must_use]
pub fn new(capacity: usize) -> Self {
let (tx, _) = broadcast::channel(capacity);
Self { tx }
}
pub fn send(&self, event: JobEvent) -> usize {
self.tx.send(event).unwrap_or(0)
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<JobEvent> {
self.tx.subscribe()
}
#[must_use]
pub fn subscriber_count(&self) -> usize {
self.tx.receiver_count()
}
pub fn notify(&self, job_id: Uuid, event_type: JobEventType) -> usize {
self.send(JobEvent::new(job_id, event_type))
}
}
impl std::fmt::Debug for NotificationBus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NotificationBus")
.field("subscriber_count", &self.tx.receiver_count())
.finish()
}
}
impl Default for NotificationBus {
fn default() -> Self {
Self::new(64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::broadcast::error::TryRecvError;
#[tokio::test]
async fn test_notification_bus_subscriber_receives_events() {
let bus = NotificationBus::new(16);
let mut rx = bus.subscribe();
let job_id = Uuid::new_v4();
bus.notify(job_id, JobEventType::Queued);
let event = rx.recv().await.expect("should receive event");
assert_eq!(event.job_id, job_id);
assert_eq!(event.event_type, JobEventType::Queued);
}
#[tokio::test]
async fn test_notification_multiple_subscribers() {
let bus = NotificationBus::new(16);
let mut rx1 = bus.subscribe();
let mut rx2 = bus.subscribe();
let job_id = Uuid::new_v4();
let sent = bus.notify(job_id, JobEventType::Started);
assert_eq!(sent, 2);
let e1 = rx1.recv().await.expect("rx1 should receive");
let e2 = rx2.recv().await.expect("rx2 should receive");
assert_eq!(e1.event_type, JobEventType::Started);
assert_eq!(e2.event_type, JobEventType::Started);
}
#[test]
fn test_no_subscriber_send_returns_zero() {
let bus = NotificationBus::new(8);
let sent = bus.notify(Uuid::new_v4(), JobEventType::Completed);
assert_eq!(sent, 0);
}
#[tokio::test]
async fn test_terminal_event_types() {
assert!(JobEventType::Completed.is_terminal());
assert!(JobEventType::Failed.is_terminal());
assert!(JobEventType::Cancelled.is_terminal());
assert!(!JobEventType::Queued.is_terminal());
assert!(!JobEventType::Started.is_terminal());
}
#[tokio::test]
async fn test_event_sequence_in_order() {
let bus = NotificationBus::new(16);
let mut rx = bus.subscribe();
let job_id = Uuid::new_v4();
let types = [
JobEventType::Queued,
JobEventType::Started,
JobEventType::Completed,
];
for &t in &types {
bus.notify(job_id, t);
}
for &expected in &types {
let event = rx.recv().await.expect("should receive");
assert_eq!(event.event_type, expected);
}
}
#[test]
fn test_subscriber_count_tracks_receivers() {
let bus = NotificationBus::new(8);
assert_eq!(bus.subscriber_count(), 0);
let _rx1 = bus.subscribe();
assert_eq!(bus.subscriber_count(), 1);
let _rx2 = bus.subscribe();
assert_eq!(bus.subscriber_count(), 2);
}
#[tokio::test]
async fn test_dropped_subscriber_does_not_receive() {
let bus = NotificationBus::new(8);
let mut rx = bus.subscribe();
{
let _dropped = bus.subscribe();
}
bus.notify(Uuid::new_v4(), JobEventType::Failed);
let event = rx.try_recv();
assert!(event.is_ok() || matches!(event, Err(TryRecvError::Empty)));
}
#[test]
fn test_event_labels() {
assert_eq!(JobEventType::Queued.label(), "queued");
assert_eq!(JobEventType::Started.label(), "started");
assert_eq!(JobEventType::Completed.label(), "completed");
assert_eq!(JobEventType::Failed.label(), "failed");
assert_eq!(JobEventType::Cancelled.label(), "cancelled");
}
#[test]
fn test_default_bus_capacity() {
let bus = NotificationBus::default();
let _rx = bus.subscribe();
}
}