Skip to main content

execution_policy/
event.rs

1//! Observability hook. Dependency-free: `on_event` is a plain synchronous
2//! callback. `Event` values are constructed **only** when a hook is registered,
3//! so the happy path stays allocation-free when no hook is set.
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use crate::error::BreakerState;
9
10/// A lifecycle event emitted by the engine when a hook is registered.
11#[non_exhaustive]
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Event {
14    /// An attempt's operation returned an error that will be classified.
15    AttemptFailed { attempt: u32 },
16    /// An attempt exceeded `attempt_timeout`.
17    AttemptTimedOut { attempt: u32 },
18    /// A retry was scheduled after `delay`.
19    RetryScheduled { attempt: u32, delay: Duration },
20    /// The operation eventually succeeded.
21    Succeeded { attempts: u32 },
22    /// Retries were exhausted (attempt cap, max_elapsed, or total timeout).
23    GaveUp { attempts: u32 },
24    /// The circuit breaker changed state.
25    CircuitStateChanged { to: BreakerState },
26    /// A call was rejected by the concurrency limit.
27    ConcurrencyRejected,
28    /// A retry was denied because the shared retry budget was exhausted.
29    RetryBudgetExhausted { attempts: u32 },
30}
31
32/// A registered event callback. Synchronous and cheap by contract; a panicking
33/// hook is the caller's bug and is **not** caught (fail fast).
34pub(crate) type EventHook = Arc<dyn Fn(&Event) + Send + Sync>;
35
36/// Emit `make()`'s event to `hook` — but only construct the event if a hook is
37/// present. This is the zero-cost-when-absent guard.
38#[inline]
39pub(crate) fn emit(hook: &Option<EventHook>, make: impl FnOnce() -> Event) {
40    if let Some(h) = hook {
41        h(&make());
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use std::sync::Mutex;
49
50    #[test]
51    fn emit_skips_when_no_hook() {
52        let none: Option<EventHook> = None;
53        // make() must not be called — use a side effect to prove it.
54        let called = std::cell::Cell::new(false);
55        emit(&none, || {
56            called.set(true);
57            Event::Succeeded { attempts: 1 }
58        });
59        assert!(
60            !called.get(),
61            "event must not be constructed without a hook"
62        );
63    }
64
65    #[test]
66    fn emit_invokes_hook() {
67        let seen: Arc<Mutex<Vec<Event>>> = Arc::new(Mutex::new(Vec::new()));
68        let sink = Arc::clone(&seen);
69        let hook: Option<EventHook> = Some(Arc::new(move |e: &Event| {
70            sink.lock().unwrap().push(e.clone())
71        }));
72        emit(&hook, || Event::Succeeded { attempts: 3 });
73        assert_eq!(
74            seen.lock().unwrap().as_slice(),
75            &[Event::Succeeded { attempts: 3 }]
76        );
77    }
78}