Skip to main content

taktora_executor/
observer.rs

1//! `Observer` trait — lifecycle hooks invoked by the executor.
2
3use crate::error::ExecutorError;
4use crate::fault::{ExecutorFaultReason, FaultReason};
5use crate::stats::CycleObservation;
6use crate::task_id::TaskId;
7
8/// Generic user event carried by [`Observer::on_send_event`].
9///
10/// # Construction
11///
12/// Use [`UserEvent::new`] to create a value; struct literal syntax is not
13/// available from outside this crate because `UserEvent` is `#[non_exhaustive]`.
14#[derive(Clone, Debug, Default)]
15#[non_exhaustive]
16pub struct UserEvent {
17    /// User-defined event kind.
18    pub kind: u32,
19    /// Numeric payload.
20    pub int_data: i64,
21    /// Optional string payload.
22    pub string_data: Option<String>,
23}
24
25impl UserEvent {
26    /// Create a new event with the given `kind` and `int_data`.
27    #[must_use]
28    pub const fn new(kind: u32, int_data: i64) -> Self {
29        Self {
30            kind,
31            int_data,
32            string_data: None,
33        }
34    }
35
36    /// Attach an optional string payload to this event.
37    #[must_use]
38    pub fn with_string(mut self, s: impl Into<String>) -> Self {
39        self.string_data = Some(s.into());
40        self
41    }
42}
43
44/// Lifecycle observer invoked by the executor at well-defined points.
45///
46/// # Why two hooks?
47///
48/// This handles coarse, infrequent lifecycle events — executor up/down,
49/// app start/stop, faults, and user events — so its callbacks can afford
50/// to do real work. For per-`execute` timing on the dispatch hot path
51/// (which must stay cheap), use [`crate::ExecutionMonitor`] instead.
52///
53/// All methods have no-op defaults. The executor never blocks on observer
54/// callbacks — heavy work should be queued internally.
55pub trait Observer: Send + Sync {
56    /// Called once just before the dispatch loop begins.
57    fn on_executor_up(&self) {}
58    /// Called once just after the dispatch loop finishes cleanly.
59    fn on_executor_down(&self) {}
60    /// Called when the dispatch loop returns an error.
61    fn on_executor_error(&self, _e: &ExecutorError) {}
62
63    /// Called before an item with `app_id().is_some()` runs (per invocation).
64    fn on_app_start(&self, _task: TaskId, _app: u32, _instance: Option<u32>) {}
65    /// Called after such an item runs.
66    fn on_app_stop(&self, _task: TaskId) {}
67    /// Called when an item returns `Err` or panics.
68    fn on_app_error(&self, _task: TaskId, _e: &(dyn std::error::Error + 'static)) {}
69
70    /// Called when an item invokes `Context::send_event`.
71    fn on_send_event(&self, _task: TaskId, _ev: UserEvent) {}
72
73    /// Called once when a task transitions from `Running` to `Faulted`
74    /// (per-task budget overrun, `REQ_0070`). The cascade transition
75    /// triggered by an executor-wide fault does NOT fire this hook —
76    /// see [`Observer::on_executor_fault`]. `REQ_0073`.
77    fn on_task_fault(&self, _task: TaskId, _reason: FaultReason) {}
78
79    /// Called once when a task transitions from `Faulted` back to
80    /// `Running` (manual clear via `Executor::clear_task_fault`).
81    fn on_task_clear(&self, _task: TaskId) {}
82
83    /// Called once when the executor transitions from `Running` to
84    /// `Faulted` (executor-wide iteration budget breach, `REQ_0071`).
85    fn on_executor_fault(&self, _reason: ExecutorFaultReason) {}
86
87    /// Called once when the executor transitions from `Faulted` back
88    /// to `Running` (manual clear via `Executor::clear_executor_fault`).
89    fn on_executor_clear(&self) {}
90
91    /// Fires once per scan cycle of a cyclic task, including a faulted scan
92    /// (`REQ_0103`, `REQ_0107`). Default no-op for backward compatibility.
93    ///
94    /// **Containment:** runs on the executor's `WaitSet` thread outside the
95    /// per-item panic catch — a panic here routes to the fail-fast boundary
96    /// (`REQ_0123`). Implementations must not panic.
97    fn on_cycle_stats(&self, _obs: &CycleObservation) {}
98}
99
100/// No-op observer used when the user does not configure one.
101pub struct NoopObserver;
102impl Observer for NoopObserver {}
103
104#[cfg(test)]
105mod cycle_stats_hook_tests {
106    use super::*;
107    use crate::TaskId;
108    use crate::stats::CycleObservation;
109    use std::sync::Arc;
110    use std::sync::atomic::{AtomicU64, Ordering};
111
112    struct CountingObs(Arc<AtomicU64>);
113    impl Observer for CountingObs {
114        fn on_cycle_stats(&self, _: &CycleObservation) {
115            self.0.fetch_add(1, Ordering::Relaxed);
116        }
117    }
118
119    fn sample_obs() -> CycleObservation {
120        CycleObservation {
121            cycle_index: 0,
122            task_id: TaskId::from("t"),
123            task_index: 0,
124            faulted: false,
125            period_ns: 0,
126            pre_ns: 0,
127            actual_period_ns: None,
128            jitter_ns: None,
129            lateness_ns: None,
130            skipped_slots: 0,
131            took_ns: None,
132        }
133    }
134
135    #[test]
136    fn default_on_cycle_stats_is_noop() {
137        let noop = NoopObserver;
138        noop.on_cycle_stats(&sample_obs()); // default no-op: must compile & not panic
139    }
140
141    #[test]
142    fn overridden_on_cycle_stats_fires() {
143        let n = Arc::new(AtomicU64::new(0));
144        let c = CountingObs(Arc::clone(&n));
145        c.on_cycle_stats(&sample_obs());
146        assert_eq!(n.load(Ordering::Relaxed), 1);
147    }
148}