Skip to main content

nexus_rt/
catch_unwind.rs

1//! Panic-catching annotation for handlers.
2
3use std::panic::AssertUnwindSafe;
4
5use crate::handler::Handler;
6use crate::world::World;
7
8/// Panic-catching wrapper for [`Handler`] implementations.
9///
10/// Catches panics during [`run()`](Handler::run) so the handler is never
11/// lost during move-out-fire dispatch. This is an annotation — wrap a
12/// concrete handler, then virtualize through your chosen storage (`Box`,
13/// `Flat`, `Flex`, typed slab, etc.).
14///
15/// By constructing this wrapper, the user asserts that the inner handler
16/// (and any [`World`] resources it mutates) can tolerate partial writes
17/// caused by an unwound `run()` call. This is the same assertion as
18/// [`std::panic::AssertUnwindSafe`], applied at the handler level.
19///
20/// # Examples
21///
22/// ```
23/// use nexus_rt::{CatchAssertUnwindSafe, WorldBuilder, ResMut, IntoHandler, Handler, Virtual, Resource};
24///
25/// #[derive(Resource)]
26/// struct Counter(u64);
27///
28/// fn tick(mut counter: ResMut<Counter>, event: u32) {
29///     counter.0 += event as u64;
30/// }
31///
32/// let mut builder = WorldBuilder::new();
33/// builder.register(Counter(0));
34/// let mut world = builder.build();
35///
36/// let handler = tick.into_handler(world.registry());
37/// let guarded = CatchAssertUnwindSafe::new(handler);
38/// let mut boxed: Virtual<u32> = Box::new(guarded);
39///
40/// boxed.run(&mut world, 10);
41/// assert_eq!(world.resource::<Counter>().0, 10);
42/// ```
43pub struct CatchAssertUnwindSafe<H> {
44    handler: H,
45    /// Number of panics caught since construction.
46    panic_count: u64,
47}
48
49impl<H> CatchAssertUnwindSafe<H> {
50    /// Wrap a handler with panic catching.
51    ///
52    /// The caller asserts that the handler and any resources it touches
53    /// are safe to continue using after a caught panic.
54    pub fn new(handler: H) -> Self {
55        Self {
56            handler,
57            panic_count: 0,
58        }
59    }
60
61    /// Number of panics caught since construction.
62    ///
63    /// Poll this periodically in health checks to detect handlers that
64    /// are silently failing. A non-zero value means the handler panicked
65    /// at least once — investigate the root cause.
66    pub fn panic_count(&self) -> u64 {
67        self.panic_count
68    }
69}
70
71impl<E, H: Handler<E>> Handler<E> for CatchAssertUnwindSafe<H> {
72    fn run(&mut self, world: &mut World, event: E) {
73        let handler = &mut self.handler;
74        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
75            handler.run(world, event);
76        }));
77        if result.is_err() {
78            self.panic_count = self.panic_count.saturating_add(1);
79        }
80    }
81
82    fn name(&self) -> &'static str {
83        self.handler.name()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::{IntoHandler, ResMut, WorldBuilder};
91
92    fn normal_handler(mut val: ResMut<u64>, event: u64) {
93        *val += event;
94    }
95
96    #[test]
97    fn forwards_run() {
98        let mut builder = WorldBuilder::new();
99        builder.register::<u64>(0);
100        let mut world = builder.build();
101
102        let handler = normal_handler.into_handler(world.registry());
103        let mut guarded = CatchAssertUnwindSafe::new(handler);
104
105        guarded.run(&mut world, 10);
106        assert_eq!(*world.resource::<u64>(), 10);
107    }
108
109    fn panicking_handler(_val: ResMut<u64>, _event: u64) {
110        panic!("boom");
111    }
112
113    #[test]
114    fn survives_panic() {
115        let mut builder = WorldBuilder::new();
116        builder.register::<u64>(0);
117        let mut world = builder.build();
118
119        let handler = panicking_handler.into_handler(world.registry());
120        let mut guarded = CatchAssertUnwindSafe::new(handler);
121
122        // Should not panic — caught internally.
123        guarded.run(&mut world, 10);
124
125        // Handler survives — can be called again.
126        guarded.run(&mut world, 10);
127    }
128
129    fn partial_write_then_panic(mut val: ResMut<u64>, event: u64) {
130        *val += event;
131        panic!("mid-flight");
132    }
133
134    #[test]
135    fn world_state_survives_handler_panic() {
136        let mut builder = WorldBuilder::new();
137        builder.register::<u64>(0);
138        let mut world = builder.build();
139
140        let handler = partial_write_then_panic.into_handler(world.registry());
141        let mut guarded = CatchAssertUnwindSafe::new(handler);
142
143        // First call: writes 10, then panics. The write is visible.
144        guarded.run(&mut world, 10);
145        assert_eq!(*world.resource::<u64>(), 10);
146
147        // World is not corrupted — normal handler works afterwards.
148        let handler2 = normal_handler.into_handler(world.registry());
149        let mut guarded2 = CatchAssertUnwindSafe::new(handler2);
150        guarded2.run(&mut world, 5);
151        assert_eq!(*world.resource::<u64>(), 15);
152
153        // The panicking handler can be called again.
154        guarded.run(&mut world, 3);
155        assert_eq!(*world.resource::<u64>(), 18);
156    }
157
158    #[test]
159    fn forwards_name() {
160        let mut builder = WorldBuilder::new();
161        builder.register::<u64>(0);
162        let world = builder.build();
163
164        let handler = normal_handler.into_handler(world.registry());
165        let guarded = CatchAssertUnwindSafe::new(handler);
166
167        assert!(guarded.name().contains("normal_handler"));
168    }
169}