1use std::collections::HashMap;
42use std::future::Future;
43use std::pin::Pin;
44use std::sync::{Arc, Mutex, RwLock};
45
46use serde::Serialize;
47use serde::de::DeserializeOwned;
48use serde_json::Value;
49
50use crate::{AppState, AutumnError, AutumnResult};
51
52pub trait Event: Serialize + DeserializeOwned + Send + Sync + 'static {
58 const NAME: &'static str;
61}
62
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65pub enum DispatchMode {
66 Sync,
68 Durable,
70}
71
72pub type ListenerHandler =
77 fn(AppState, Value) -> Pin<Box<dyn Future<Output = AutumnResult<()>> + Send + 'static>>;
78
79#[derive(Clone)]
84pub struct ListenerInfo {
85 pub event_name: &'static str,
87 pub listener_name: String,
89 pub mode: DispatchMode,
91 pub job_name: Option<String>,
93 pub max_attempts: u32,
95 pub initial_backoff_ms: u64,
97 pub handler: ListenerHandler,
99}
100
101#[derive(Clone, Default)]
106pub struct EventRegistry {
107 by_event: Arc<HashMap<&'static str, Vec<ListenerInfo>>>,
108}
109
110impl EventRegistry {
111 #[must_use]
113 pub fn from_listeners(listeners: Vec<ListenerInfo>) -> Self {
114 let mut by_event: HashMap<&'static str, Vec<ListenerInfo>> = HashMap::new();
115 for listener in listeners {
116 by_event
117 .entry(listener.event_name)
118 .or_default()
119 .push(listener);
120 }
121 Self {
122 by_event: Arc::new(by_event),
123 }
124 }
125
126 #[must_use]
128 pub fn listeners_for(&self, event_name: &str) -> &[ListenerInfo] {
129 self.by_event.get(event_name).map_or(&[][..], Vec::as_slice)
130 }
131
132 #[must_use]
140 pub fn durable_job_infos(&self) -> Vec<crate::job::JobInfo> {
141 self.by_event
142 .values()
143 .flatten()
144 .filter(|listener| listener.mode == DispatchMode::Durable)
145 .map(|listener| crate::job::JobInfo {
146 name: listener
147 .job_name
148 .clone()
149 .expect("durable listener must carry a job_name"),
150 max_attempts: listener.max_attempts,
151 initial_backoff_ms: listener.initial_backoff_ms,
152 queue: "default".to_string(),
153 uniqueness: None,
154 concurrency: None,
155 version: 1,
156 handler: listener.handler,
157 })
158 .collect()
159 }
160}
161
162#[derive(Clone, Debug)]
164pub struct RecordedEvent {
165 pub event_name: &'static str,
167 pub payload: Value,
169}
170
171#[derive(Default)]
174pub struct EventRecorder {
175 events: Mutex<Vec<RecordedEvent>>,
176}
177
178impl EventRecorder {
179 fn record(&self, event_name: &'static str, payload: Value) {
180 self.events
181 .lock()
182 .expect("event recorder lock poisoned")
183 .push(RecordedEvent {
184 event_name,
185 payload,
186 });
187 }
188
189 #[must_use]
195 pub fn published<E: Event>(&self) -> Vec<E> {
196 self.events
197 .lock()
198 .expect("event recorder lock poisoned")
199 .iter()
200 .filter(|recorded| recorded.event_name == E::NAME)
201 .filter_map(|recorded| serde_json::from_value(recorded.payload.clone()).ok())
202 .collect()
203 }
204
205 #[must_use]
211 pub fn count<E: Event>(&self) -> usize {
212 self.events
213 .lock()
214 .expect("event recorder lock poisoned")
215 .iter()
216 .filter(|recorded| recorded.event_name == E::NAME)
217 .count()
218 }
219
220 #[must_use]
226 pub fn all(&self) -> Vec<RecordedEvent> {
227 self.events
228 .lock()
229 .expect("event recorder lock poisoned")
230 .clone()
231 }
232}
233
234#[derive(Clone)]
239pub struct Events {
240 registry: Arc<EventRegistry>,
241 recorder: Option<Arc<EventRecorder>>,
242 state: AppState,
243}
244
245impl Events {
246 pub async fn publish<E: Event>(&self, event: E) -> AutumnResult<()> {
258 let payload = serialize_event(&event)?;
259 dispatch(
260 &self.registry,
261 self.recorder.as_deref(),
262 &self.state,
263 E::NAME,
264 payload,
265 )
266 .await
267 }
268}
269
270impl axum::extract::FromRequestParts<AppState> for Events {
271 type Rejection = AutumnError;
272
273 async fn from_request_parts(
274 _parts: &mut http::request::Parts,
275 state: &AppState,
276 ) -> Result<Self, Self::Rejection> {
277 let registry = state
279 .extension::<EventRegistry>()
280 .unwrap_or_else(|| Arc::new(EventRegistry::default()));
281 let recorder = state.extension::<EventRecorder>();
282 Ok(Self {
283 registry,
284 recorder,
285 state: state.clone(),
286 })
287 }
288}
289
290fn serialize_event<E: Event>(event: &E) -> AutumnResult<Value> {
291 serde_json::to_value(event).map_err(|e| {
292 AutumnError::internal_server_error(std::io::Error::other(format!(
293 "event serialization failed: {e}"
294 )))
295 })
296}
297
298async fn dispatch(
300 registry: &EventRegistry,
301 recorder: Option<&EventRecorder>,
302 state: &AppState,
303 event_name: &'static str,
304 payload: Value,
305) -> AutumnResult<()> {
306 if let Some(recorder) = recorder {
308 recorder.record(event_name, payload.clone());
309 }
310
311 let listeners = registry.listeners_for(event_name);
312 if listeners.is_empty() {
313 return Ok(());
314 }
315
316 let app_client = state.extension::<crate::job::JobClient>();
324 let mut durable_error = None;
325 for listener in listeners
326 .iter()
327 .filter(|listener| listener.mode == DispatchMode::Durable)
328 {
329 let job_name = listener
330 .job_name
331 .as_deref()
332 .expect("durable listener must carry a job_name");
333 let enqueued = if let Some(client) = &app_client {
339 client.enqueue_after_commit(job_name, payload.clone()).await
340 } else {
341 crate::job::enqueue_after_commit(job_name, payload.clone()).await
342 };
343 if let Err(error) = enqueued
347 && durable_error.is_none()
348 {
349 durable_error = Some(error);
350 }
351 }
352
353 run_sync_listeners(state, listeners, &payload).await;
356
357 durable_error.map_or(Ok(()), Err)
358}
359
360async fn run_sync_listeners(state: &AppState, listeners: &[ListenerInfo], payload: &Value) {
371 use futures::FutureExt as _;
372
373 let runs = listeners
374 .iter()
375 .filter(|listener| listener.mode == DispatchMode::Sync)
376 .map(|listener| {
377 let state = state.clone();
378 let payload = payload.clone();
379 let run = listener.handler;
380 let name = listener.listener_name.clone();
381 async move {
382 match std::panic::AssertUnwindSafe(run(state, payload))
383 .catch_unwind()
384 .await
385 {
386 Ok(Ok(())) => {}
387 Ok(Err(error)) => {
388 tracing::error!(listener = %name, %error, "sync event listener failed");
389 }
390 Err(_panic) => {
391 tracing::error!(listener = %name, "sync event listener panicked");
392 }
393 }
394 }
395 });
396 futures::future::join_all(runs).await;
397}
398
399struct GlobalBus {
400 registry: Arc<EventRegistry>,
401 recorder: Option<Arc<EventRecorder>>,
402 state: AppState,
403}
404
405static GLOBAL_EVENT_BUS: RwLock<Option<Arc<GlobalBus>>> = RwLock::new(None);
406
407fn global_bus() -> Option<Arc<GlobalBus>> {
408 GLOBAL_EVENT_BUS.read().ok().and_then(|guard| guard.clone())
409}
410
411pub(crate) fn init_global_event_bus(
415 registry: &EventRegistry,
416 state: &AppState,
417 recorder: Option<Arc<EventRecorder>>,
418) {
419 let bus = Arc::new(GlobalBus {
420 registry: Arc::new(registry.clone()),
421 recorder,
422 state: state.clone(),
423 });
424 if let Ok(mut guard) = GLOBAL_EVENT_BUS.write() {
425 *guard = Some(bus);
426 }
427}
428
429pub fn clear_global_event_bus() {
431 if let Ok(mut guard) = GLOBAL_EVENT_BUS.write() {
432 *guard = None;
433 }
434}
435
436tokio::task_local! {
437 static CURRENT_EVENT_APP: AppState;
440}
441
442pub(crate) fn scope_event_app<F>(
447 state: AppState,
448 future: F,
449) -> tokio::task::futures::TaskLocalFuture<AppState, F>
450where
451 F: Future,
452{
453 CURRENT_EVENT_APP.scope(state, future)
454}
455
456fn current_event_app() -> Option<AppState> {
457 CURRENT_EVENT_APP.try_with(AppState::clone).ok()
458}
459
460pub async fn publish<E: Event>(event: E) -> AutumnResult<()> {
472 let payload = serialize_event(&event)?;
473
474 if let Some(state) = current_event_app() {
476 let registry = state.extension::<EventRegistry>();
477 let empty = EventRegistry::default();
478 let registry_ref = registry.as_deref().unwrap_or(&empty);
479 let recorder = state.extension::<EventRecorder>();
480 return dispatch(registry_ref, recorder.as_deref(), &state, E::NAME, payload).await;
481 }
482
483 let Some(bus) = global_bus() else {
486 return Ok(());
487 };
488 dispatch(
489 &bus.registry,
490 bus.recorder.as_deref(),
491 &bus.state,
492 E::NAME,
493 payload,
494 )
495 .await
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use serde::Deserialize;
502 use std::sync::atomic::{AtomicU32, Ordering};
503
504 #[derive(Serialize, Deserialize, Clone, Debug)]
505 struct Ping {
506 n: i64,
507 }
508 impl Event for Ping {
509 const NAME: &'static str = "Ping";
510 }
511
512 fn ok_handler() -> ListenerHandler {
513 |_state, _payload| Box::pin(async { Ok(()) })
514 }
515
516 fn sync_listener(name: &str, handler: ListenerHandler) -> ListenerInfo {
517 ListenerInfo {
518 event_name: Ping::NAME,
519 listener_name: name.to_string(),
520 mode: DispatchMode::Sync,
521 job_name: None,
522 max_attempts: 0,
523 initial_backoff_ms: 0,
524 handler,
525 }
526 }
527
528 fn durable_listener(name: &str) -> ListenerInfo {
529 ListenerInfo {
530 event_name: Ping::NAME,
531 listener_name: name.to_string(),
532 mode: DispatchMode::Durable,
533 job_name: Some(format!("__event_listener::{name}")),
534 max_attempts: 4,
535 initial_backoff_ms: 250,
536 handler: ok_handler(),
537 }
538 }
539
540 #[test]
541 fn registry_groups_by_event_name() {
542 let registry = EventRegistry::from_listeners(vec![
543 sync_listener("a", ok_handler()),
544 sync_listener("b", ok_handler()),
545 ]);
546 assert_eq!(registry.listeners_for("Ping").len(), 2);
547 assert!(registry.listeners_for("Other").is_empty());
548 }
549
550 #[test]
551 fn durable_listeners_become_job_infos() {
552 let registry = EventRegistry::from_listeners(vec![
553 sync_listener("a", ok_handler()),
554 durable_listener("seed_workspace"),
555 ]);
556 let jobs = registry.durable_job_infos();
557 assert_eq!(jobs.len(), 1, "only durable listeners become jobs");
558 assert_eq!(jobs[0].name, "__event_listener::seed_workspace");
559 assert_eq!(jobs[0].max_attempts, 4);
560 assert_eq!(jobs[0].initial_backoff_ms, 250);
561 }
562
563 #[tokio::test]
564 async fn sync_listeners_are_isolated_from_panics_and_errors() {
565 static RAN: AtomicU32 = AtomicU32::new(0);
566 RAN.store(0, Ordering::SeqCst);
567
568 let panicking: ListenerHandler = |_state, _payload| Box::pin(async { panic!("boom") });
569 let erroring: ListenerHandler = |_state, _payload| {
570 Box::pin(async {
571 Err(AutumnError::internal_server_error(std::io::Error::other(
572 "nope",
573 )))
574 })
575 };
576 let counting: ListenerHandler = |_state, _payload| {
577 Box::pin(async {
578 RAN.fetch_add(1, Ordering::SeqCst);
579 Ok(())
580 })
581 };
582
583 let registry = EventRegistry::from_listeners(vec![
584 sync_listener("panics", panicking),
585 sync_listener("errors", erroring),
586 sync_listener("counts", counting),
587 ]);
588 let state = AppState::for_test();
589
590 let result = dispatch(
593 ®istry,
594 None,
595 &state,
596 Ping::NAME,
597 serde_json::json!({"n": 1}),
598 )
599 .await;
600 assert!(result.is_ok(), "publish stays Ok despite listener failures");
601 assert_eq!(RAN.load(Ordering::SeqCst), 1, "surviving listener ran");
602 }
603
604 #[tokio::test]
605 async fn sync_listeners_run_even_when_a_durable_enqueue_fails() {
606 static RAN: AtomicU32 = AtomicU32::new(0);
610 crate::job::clear_global_job_client();
611 RAN.store(0, Ordering::SeqCst);
612 let counting: ListenerHandler = |_state, _payload| {
613 Box::pin(async {
614 RAN.fetch_add(1, Ordering::SeqCst);
615 Ok(())
616 })
617 };
618 let registry = EventRegistry::from_listeners(vec![
619 durable_listener("seed_workspace"),
620 sync_listener("counts", counting),
621 ]);
622 let state = AppState::for_test();
623 let _ = dispatch(
624 ®istry,
625 None,
626 &state,
627 Ping::NAME,
628 serde_json::json!({"n": 1}),
629 )
630 .await;
631 assert_eq!(RAN.load(Ordering::SeqCst), 1, "sync listener ran anyway");
633 }
634
635 #[tokio::test]
636 async fn missing_listener_is_a_noop() {
637 let registry = EventRegistry::default();
638 let state = AppState::for_test();
639 let result = dispatch(
640 ®istry,
641 None,
642 &state,
643 Ping::NAME,
644 serde_json::json!({"n": 1}),
645 )
646 .await;
647 assert!(result.is_ok());
648 }
649
650 #[tokio::test]
651 async fn recorder_captures_published_events() {
652 let registry = EventRegistry::default();
653 let recorder = EventRecorder::default();
654 let state = AppState::for_test();
655 let payload = serialize_event(&Ping { n: 7 }).unwrap();
656 dispatch(®istry, Some(&recorder), &state, Ping::NAME, payload)
657 .await
658 .unwrap();
659 assert_eq!(recorder.count::<Ping>(), 1);
660 let published = recorder.published::<Ping>();
661 assert_eq!(published.len(), 1);
662 assert_eq!(published[0].n, 7);
663 }
664}