oximedia_distributed/
notifications.rs1use std::time::SystemTime;
26use tokio::sync::broadcast;
27use uuid::Uuid;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31pub enum JobEventType {
32 Queued,
34 Started,
36 Completed,
38 Failed,
40 Cancelled,
42}
43
44impl JobEventType {
45 #[must_use]
47 pub fn label(self) -> &'static str {
48 match self {
49 Self::Queued => "queued",
50 Self::Started => "started",
51 Self::Completed => "completed",
52 Self::Failed => "failed",
53 Self::Cancelled => "cancelled",
54 }
55 }
56
57 #[must_use]
59 pub fn is_terminal(self) -> bool {
60 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
61 }
62}
63
64impl std::fmt::Display for JobEventType {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.label())
67 }
68}
69
70#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
72pub struct JobEvent {
73 pub job_id: Uuid,
75 pub event_type: JobEventType,
77 #[serde(with = "system_time_serde")]
79 pub timestamp: SystemTime,
80}
81
82impl JobEvent {
83 #[must_use]
85 pub fn new(job_id: Uuid, event_type: JobEventType) -> Self {
86 Self {
87 job_id,
88 event_type,
89 timestamp: SystemTime::now(),
90 }
91 }
92
93 #[must_use]
95 pub fn is_terminal(&self) -> bool {
96 self.event_type.is_terminal()
97 }
98}
99
100mod system_time_serde {
101 use serde::{Deserialize, Deserializer, Serializer};
102 use std::time::{Duration, SystemTime, UNIX_EPOCH};
103
104 pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
105 let epoch_secs = t
106 .duration_since(UNIX_EPOCH)
107 .unwrap_or(Duration::ZERO)
108 .as_secs();
109 s.serialize_u64(epoch_secs)
110 }
111
112 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
113 let secs = u64::deserialize(d)?;
114 Ok(UNIX_EPOCH + Duration::from_secs(secs))
115 }
116}
117
118pub struct NotificationBus {
125 tx: broadcast::Sender<JobEvent>,
126}
127
128impl NotificationBus {
129 #[must_use]
134 pub fn new(capacity: usize) -> Self {
135 let (tx, _) = broadcast::channel(capacity);
136 Self { tx }
137 }
138
139 pub fn send(&self, event: JobEvent) -> usize {
144 self.tx.send(event).unwrap_or(0)
145 }
146
147 #[must_use]
152 pub fn subscribe(&self) -> broadcast::Receiver<JobEvent> {
153 self.tx.subscribe()
154 }
155
156 #[must_use]
158 pub fn subscriber_count(&self) -> usize {
159 self.tx.receiver_count()
160 }
161
162 pub fn notify(&self, job_id: Uuid, event_type: JobEventType) -> usize {
164 self.send(JobEvent::new(job_id, event_type))
165 }
166}
167
168impl std::fmt::Debug for NotificationBus {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 f.debug_struct("NotificationBus")
171 .field("subscriber_count", &self.tx.receiver_count())
172 .finish()
173 }
174}
175
176impl Default for NotificationBus {
177 fn default() -> Self {
178 Self::new(64)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use tokio::sync::broadcast::error::TryRecvError;
186
187 #[tokio::test]
188 async fn test_notification_bus_subscriber_receives_events() {
189 let bus = NotificationBus::new(16);
190 let mut rx = bus.subscribe();
191
192 let job_id = Uuid::new_v4();
193 bus.notify(job_id, JobEventType::Queued);
194
195 let event = rx.recv().await.expect("should receive event");
196 assert_eq!(event.job_id, job_id);
197 assert_eq!(event.event_type, JobEventType::Queued);
198 }
199
200 #[tokio::test]
201 async fn test_notification_multiple_subscribers() {
202 let bus = NotificationBus::new(16);
203 let mut rx1 = bus.subscribe();
204 let mut rx2 = bus.subscribe();
205
206 let job_id = Uuid::new_v4();
207 let sent = bus.notify(job_id, JobEventType::Started);
208 assert_eq!(sent, 2);
210
211 let e1 = rx1.recv().await.expect("rx1 should receive");
212 let e2 = rx2.recv().await.expect("rx2 should receive");
213 assert_eq!(e1.event_type, JobEventType::Started);
214 assert_eq!(e2.event_type, JobEventType::Started);
215 }
216
217 #[test]
218 fn test_no_subscriber_send_returns_zero() {
219 let bus = NotificationBus::new(8);
220 let sent = bus.notify(Uuid::new_v4(), JobEventType::Completed);
222 assert_eq!(sent, 0);
223 }
224
225 #[tokio::test]
226 async fn test_terminal_event_types() {
227 assert!(JobEventType::Completed.is_terminal());
228 assert!(JobEventType::Failed.is_terminal());
229 assert!(JobEventType::Cancelled.is_terminal());
230 assert!(!JobEventType::Queued.is_terminal());
231 assert!(!JobEventType::Started.is_terminal());
232 }
233
234 #[tokio::test]
235 async fn test_event_sequence_in_order() {
236 let bus = NotificationBus::new(16);
237 let mut rx = bus.subscribe();
238
239 let job_id = Uuid::new_v4();
240 let types = [
241 JobEventType::Queued,
242 JobEventType::Started,
243 JobEventType::Completed,
244 ];
245 for &t in &types {
246 bus.notify(job_id, t);
247 }
248
249 for &expected in &types {
250 let event = rx.recv().await.expect("should receive");
251 assert_eq!(event.event_type, expected);
252 }
253 }
254
255 #[test]
256 fn test_subscriber_count_tracks_receivers() {
257 let bus = NotificationBus::new(8);
258 assert_eq!(bus.subscriber_count(), 0);
259 let _rx1 = bus.subscribe();
260 assert_eq!(bus.subscriber_count(), 1);
261 let _rx2 = bus.subscribe();
262 assert_eq!(bus.subscriber_count(), 2);
263 }
264
265 #[tokio::test]
266 async fn test_dropped_subscriber_does_not_receive() {
267 let bus = NotificationBus::new(8);
268 let mut rx = bus.subscribe();
269
270 {
272 let _dropped = bus.subscribe();
273 }
274
275 bus.notify(Uuid::new_v4(), JobEventType::Failed);
276 let event = rx.try_recv();
277 assert!(event.is_ok() || matches!(event, Err(TryRecvError::Empty)));
278 }
279
280 #[test]
281 fn test_event_labels() {
282 assert_eq!(JobEventType::Queued.label(), "queued");
283 assert_eq!(JobEventType::Started.label(), "started");
284 assert_eq!(JobEventType::Completed.label(), "completed");
285 assert_eq!(JobEventType::Failed.label(), "failed");
286 assert_eq!(JobEventType::Cancelled.label(), "cancelled");
287 }
288
289 #[test]
290 fn test_default_bus_capacity() {
291 let bus = NotificationBus::default();
292 let _rx = bus.subscribe();
294 }
295}