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}