Skip to main content

nmbrs_errorhandler/
handlers.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Built-in error handler implementations.
5
6use std::sync::atomic::AtomicPtr;
7
8/// Global log function pointer. Set by the runtime to route
9/// error handler messages through the observer instead of stderr.
10/// Default: write to stderr.
11static LOG_FN: AtomicPtr<()> = AtomicPtr::new(default_log as *mut ());
12
13type LogFn = fn(&str);
14
15fn default_log(msg: &str) {
16    eprintln!("{msg}");
17}
18
19/// Set the global log function for error handlers.
20pub fn set_log_fn(f: LogFn) {
21    LOG_FN.store(f as *mut (), std::sync::atomic::Ordering::Release);
22}
23
24fn emit(msg: &str) {
25    let ptr = LOG_FN.load(std::sync::atomic::Ordering::Acquire);
26    let f: LogFn = unsafe { std::mem::transmute(ptr) };
27    f(msg);
28}
29
30use std::collections::HashMap;
31use std::sync::Mutex;
32use std::sync::atomic::{AtomicU64, Ordering};
33
34use crate::detail::ErrorDetail;
35use crate::handler::ErrorHandler;
36
37/// Signal that execution should stop after this error.
38/// Does NOT print — that's `warn`'s job. Use `warn,stop` to both log and halt.
39pub struct StopHandler;
40
41impl ErrorHandler for StopHandler {
42    fn handle(
43        &self,
44        _name: &str,
45        _error_msg: &str,
46        _cycle: u64,
47        _duration_nanos: u64,
48        detail: ErrorDetail,
49    ) -> ErrorDetail {
50        detail.with_stop()
51    }
52}
53
54/// Log a warning and pass through.
55pub struct WarnHandler;
56
57impl ErrorHandler for WarnHandler {
58    fn handle(
59        &self,
60        name: &str,
61        error_msg: &str,
62        cycle: u64,
63        _duration_nanos: u64,
64        detail: ErrorDetail,
65    ) -> ErrorDetail {
66        emit(&format!(
67            "WARN error at cycle {cycle}: [{name}] {error_msg}"
68        ));
69        detail
70    }
71}
72
73/// Log an error and pass through.
74pub struct ErrorLogHandler;
75
76impl ErrorHandler for ErrorLogHandler {
77    fn handle(
78        &self,
79        name: &str,
80        error_msg: &str,
81        cycle: u64,
82        _duration_nanos: u64,
83        detail: ErrorDetail,
84    ) -> ErrorDetail {
85        emit(&format!("ERROR at cycle {cycle}: [{name}] {error_msg}"));
86        detail
87    }
88}
89
90/// Silently pass through (no-op).
91pub struct IgnoreHandler;
92
93impl ErrorHandler for IgnoreHandler {
94    fn handle(
95        &self,
96        _name: &str,
97        _error_msg: &str,
98        _cycle: u64,
99        _duration_nanos: u64,
100        detail: ErrorDetail,
101    ) -> ErrorDetail {
102        detail
103    }
104}
105
106/// Mark the error as retryable.
107pub struct RetryHandler;
108
109impl ErrorHandler for RetryHandler {
110    fn handle(
111        &self,
112        _name: &str,
113        _error_msg: &str,
114        _cycle: u64,
115        _duration_nanos: u64,
116        detail: ErrorDetail,
117    ) -> ErrorDetail {
118        detail.with_retryable()
119    }
120}
121
122/// Count errors by type name.
123pub struct CounterHandler {
124    counts: Mutex<HashMap<String, AtomicU64>>,
125}
126
127impl Default for CounterHandler {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl CounterHandler {
134    pub fn new() -> Self {
135        Self {
136            counts: Mutex::new(HashMap::new()),
137        }
138    }
139
140    /// Get the current count for a specific error name.
141    #[allow(dead_code)]
142    pub fn get_count(&self, name: &str) -> u64 {
143        let counts = self.counts.lock().unwrap_or_else(|e| e.into_inner());
144        counts
145            .get(name)
146            .map(|c| c.load(Ordering::Relaxed))
147            .unwrap_or(0)
148    }
149
150    /// Get all error counts.
151    #[allow(dead_code)]
152    pub fn all_counts(&self) -> HashMap<String, u64> {
153        let counts = self.counts.lock().unwrap_or_else(|e| e.into_inner());
154        counts
155            .iter()
156            .map(|(k, v)| (k.clone(), v.load(Ordering::Relaxed)))
157            .collect()
158    }
159}
160
161impl ErrorHandler for CounterHandler {
162    fn handle(
163        &self,
164        name: &str,
165        _error_msg: &str,
166        _cycle: u64,
167        _duration_nanos: u64,
168        detail: ErrorDetail,
169    ) -> ErrorDetail {
170        let mut counts = self.counts.lock().unwrap_or_else(|e| e.into_inner());
171        counts
172            .entry(name.to_string())
173            .or_insert_with(|| AtomicU64::new(0))
174            .fetch_add(1, Ordering::Relaxed);
175        detail
176    }
177}
178
179/// Look up a built-in handler by name.
180pub fn builtin_handler(name: &str) -> Option<Box<dyn ErrorHandler>> {
181    match name {
182        "stop" => Some(Box::new(StopHandler)),
183        "warn" => Some(Box::new(WarnHandler)),
184        "error" => Some(Box::new(ErrorLogHandler)),
185        "ignore" => Some(Box::new(IgnoreHandler)),
186        "retry" => Some(Box::new(RetryHandler)),
187        "counter" | "count" => Some(Box::new(CounterHandler::new())),
188        _ => None,
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn retry_handler_sets_retryable() {
198        let h = RetryHandler;
199        let detail = ErrorDetail::non_retryable("test");
200        let result = h.handle("test", "msg", 0, 0, detail);
201        assert!(result.is_retryable());
202    }
203
204    #[test]
205    fn ignore_handler_passes_through() {
206        let h = IgnoreHandler;
207        let detail = ErrorDetail::non_retryable("test");
208        let result = h.handle("test", "msg", 0, 0, detail);
209        assert!(!result.is_retryable());
210        assert_eq!(result.result_code, 127);
211    }
212
213    #[test]
214    fn counter_handler_counts() {
215        let h = CounterHandler::new();
216        let detail = ErrorDetail::non_retryable("TimeoutError");
217        h.handle("TimeoutError", "timed out", 1, 0, detail.clone());
218        h.handle("TimeoutError", "timed out", 2, 0, detail.clone());
219        h.handle("OtherError", "other", 3, 0, detail);
220        assert_eq!(h.get_count("TimeoutError"), 2);
221        assert_eq!(h.get_count("OtherError"), 1);
222        assert_eq!(h.get_count("Missing"), 0);
223    }
224
225    #[test]
226    fn stop_handler_sets_should_stop() {
227        let h = StopHandler;
228        let detail = ErrorDetail::non_retryable("test");
229        let result = h.handle("test", "boom", 42, 0, detail);
230        assert!(
231            result.should_stop,
232            "stop handler should set should_stop flag"
233        );
234    }
235
236    #[test]
237    fn builtin_lookup() {
238        assert!(builtin_handler("stop").is_some());
239        assert!(builtin_handler("warn").is_some());
240        assert!(builtin_handler("error").is_some());
241        assert!(builtin_handler("ignore").is_some());
242        assert!(builtin_handler("retry").is_some());
243        assert!(builtin_handler("counter").is_some());
244        assert!(builtin_handler("count").is_some());
245        assert!(builtin_handler("bogus").is_none());
246    }
247}