Skip to main content

sprite_core/
engine.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use parking_lot::RwLock;
6use crossbeam_channel::{unbounded, Sender};
7
8use crate::actor::{Context, StateStore};
9use crate::arena::Arena;
10use crate::message::Message;
11use crate::registry::Registry;
12use crate::error::SpriteError;
13
14#[derive(Clone)]
15pub struct Handle {
16    pub id: u64,
17    pub name: String,
18    pub(crate) tx: Sender<Message>,
19}
20
21impl Handle {
22    pub fn send(&self, msg: Message) {
23        let _ = self.tx.send(msg);
24    }
25    pub fn send_msg<T: crate::util::IntoMessage>(&self, msg: T) {
26        self.send(msg.into_message());
27    }
28    pub fn request(&self, msg: Message, timeout: Duration) -> Result<crate::request::Response, SpriteError> {
29        let (req, rx) = crate::request::Request::new(msg);
30        self.send(req.payload);
31        rx.recv_timeout(timeout)
32            .map_err(|_| SpriteError::RequestTimeout)
33    }
34}
35
36pub struct Engine {
37    pub(crate) inner: Arc<EngineInner>,
38}
39
40pub(crate) struct EngineInner {
41    pub(crate) next_id: AtomicU64,
42    pub(crate) registry: Arc<Registry>,
43    pub(crate) channels: RwLock<HashMap<u64, Sender<Message>>>,
44    pub(crate) running: AtomicBool,
45}
46
47impl EngineInner {
48    pub(crate) fn new() -> Self {
49        Self {
50            next_id: AtomicU64::new(1),
51            registry: Arc::new(Registry::new()),
52            channels: RwLock::new(HashMap::new()),
53            running: AtomicBool::new(true),
54        }
55    }
56
57    pub(crate) fn send_to(&self, id: u64, msg: Message) {
58        let channels = self.channels.read();
59        if let Some(tx) = channels.get(&id) {
60            let _ = tx.send(msg);
61        }
62    }
63
64    pub(crate) fn request(&self, id: u64, msg: Message, timeout: Duration) -> Option<Message> {
65        let channels = self.channels.read();
66        if let Some(tx) = channels.get(&id) {
67            let (req, rx) = crate::request::Request::new(msg);
68            let _ = tx.send(req.payload);
69            rx.recv_timeout(timeout).ok().map(|r| r.into_message())
70        } else {
71            None
72        }
73    }
74
75    /// Spawn with defaults (used by Context::spawn)
76    pub(crate) fn spawn_simple<F>(&self, name: &str, setup: F) -> Handle
77    where F: Fn(&mut Context) + Send + Sync + 'static,
78    {
79        self.spawn(name, setup, 1024 * 64, 10, Duration::from_secs(5), Arc::new(self.clone_shallow()))
80    }
81
82    pub(crate) fn spawn<F>(
83        &self,
84        name: &str,
85        setup: F,
86        arena_size: usize,
87        max_recoveries: u32,
88        recovery_window: Duration,
89        engine_arc: Arc<EngineInner>,
90    ) -> Handle
91    where
92        F: Fn(&mut Context) + Send + Sync + 'static,
93    {
94        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
95        let (tx, rx) = unbounded();
96
97        {
98            let mut channels = self.channels.write();
99            channels.insert(id, tx.clone());
100        }
101        self.registry.register(name, id);
102
103        let state_store: StateStore = Arc::new(RwLock::new(HashMap::new()));
104        let setup = Arc::new(setup);
105        let name_owned = name.to_string();
106        let tx_for_handle = tx.clone();
107        let registry = self.registry.clone();
108        let engine_weak = Arc::downgrade(&engine_arc);
109
110        std::thread::spawn(move || {
111            let mut arena = Arena::with_capacity(arena_size);
112            let mut recovery_count = 0u32;
113            let mut last_recovery = Instant::now();
114            let mut is_first_mount = true;
115
116            loop {
117                let engine_ref = match engine_weak.upgrade() {
118                    Some(arc) => arc,
119                    None => break,
120                };
121
122                let mut ctx = Context::new(
123                    id, name_owned.clone(), state_store.clone(),
124                    rx.clone(), tx.clone(), engine_ref,
125                );
126                ctx.is_first_mount = is_first_mount;
127                let setup_clone = setup.clone();
128
129                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
130                    setup_clone(&mut ctx);
131                    if is_first_mount {
132                        if let Some(ref h) = ctx.mount_handler {
133                            h();
134                        }
135                    }
136                    is_first_mount = false;
137
138                    loop {
139                        match ctx.rx.recv_timeout(Duration::from_millis(100)) {
140                            Ok(msg) => {
141                                ctx.metrics.inc_received();
142                                if let Some(ref handler) = ctx.message_handler {
143                                    handler(msg);
144                                }
145                            }
146                            Err(_) => {}
147                        }
148                        if !ctx.engine.running.load(Ordering::SeqCst) {
149                            break;
150                        }
151                    }
152                    if let Some(ref h) = ctx.unmount_handler {
153                        h();
154                    }
155                }));
156
157                match result {
158                    Ok(()) => break,
159                    Err(_) => {
160                        ctx.metrics.inc_panic();
161                        recovery_count += 1;
162                        if recovery_count > max_recoveries && last_recovery.elapsed() < recovery_window {
163                            tracing::error!(
164                                "[Actor {}] CIRCUIT BREAKER TRIPPED after {} recoveries — halting.",
165                                id, recovery_count
166                            );
167                            break;
168                        }
169                        last_recovery = Instant::now();
170                        let start = Instant::now();
171                        arena.reset();
172                        let elapsed = start.elapsed();
173                        ctx.metrics.inc_recovery();
174                        tracing::debug!("[Actor {}] recovered in {:?}", id, elapsed);
175                        if let Some(ref h) = ctx.panic_handler {
176                            h();
177                        }
178                    }
179                }
180            }
181            registry.unregister(&name_owned);
182        });
183
184        Handle { id, name: name.to_string(), tx: tx_for_handle }
185    }
186
187    fn clone_shallow(&self) -> Self {
188        Self {
189            next_id: AtomicU64::new(self.next_id.load(Ordering::SeqCst)),
190            registry: self.registry.clone(),
191            channels: RwLock::new(self.channels.read().clone()),
192            running: AtomicBool::new(self.running.load(Ordering::SeqCst)),
193        }
194    }
195}
196
197impl Engine {
198    pub fn new() -> Self {
199        Self { inner: Arc::new(EngineInner::new()) }
200    }
201
202    pub fn spawn<F>(&self, name: &str, setup: F) -> Handle
203    where F: Fn(&mut Context) + Send + Sync + 'static,
204    {
205        self.inner.spawn(name, setup, 1024 * 64, 10, Duration::from_secs(5), self.inner.clone())
206    }
207
208    pub fn spawn_with_config<F>(
209        &self, name: &str, setup: F,
210        arena_size: usize, max_recoveries: u32, recovery_window: Duration,
211    ) -> Handle
212    where F: Fn(&mut Context) + Send + Sync + 'static,
213    {
214        self.inner.spawn(name, setup, arena_size, max_recoveries, recovery_window, self.inner.clone())
215    }
216
217    pub fn send_to(&self, id: u64, msg: Message) {
218        self.inner.send_to(id, msg);
219    }
220
221    pub fn send_named(&self, name: &str, msg: Message) {
222        if let Some(id) = self.inner.registry.lookup(name) {
223            self.inner.send_to(id, msg);
224        }
225    }
226
227    pub fn lookup(&self, name: &str) -> Option<u64> {
228        self.inner.registry.lookup(name)
229    }
230
231    pub fn broadcast(&self, msg: Message) -> usize {
232        let channels = self.inner.channels.read();
233        let mut sent = 0;
234        for (_, tx) in channels.iter() {
235            if tx.send(msg.clone()).is_ok() { sent += 1; }
236        }
237        sent
238    }
239
240    pub fn shutdown(&self) {
241        self.inner.running.store(false, Ordering::SeqCst);
242    }
243
244    pub fn is_running(&self) -> bool {
245        self.inner.running.load(Ordering::SeqCst)
246    }
247
248    pub fn actor_count(&self) -> usize {
249        self.inner.channels.read().len()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use std::time::Duration;
257
258    #[test]
259    fn spawn_and_send() {
260        let engine = Engine::new();
261        let handle = engine.spawn("test", |ctx| {
262            ctx.on_message(|msg| { println!("got: {:?}", msg); });
263        });
264        std::thread::sleep(Duration::from_millis(20));
265        handle.send(Message::text("hello"));
266        std::thread::sleep(Duration::from_millis(50));
267    }
268
269    #[test]
270    fn state_persists_across_panics() {
271        let engine = Engine::new();
272        let handle = engine.spawn("fragile", |ctx| {
273            let count = ctx.use_state("count", 0i64);
274            ctx.on_message(move |msg| {
275                if msg == "set" { count.set(42); }
276                if msg == "panic" { panic!("boom"); }
277                if msg == "check" { assert_eq!(count.get(), 42); }
278            });
279        });
280        std::thread::sleep(Duration::from_millis(20));
281        handle.send(Message::text("set"));
282        std::thread::sleep(Duration::from_millis(20));
283        handle.send(Message::text("panic"));
284        std::thread::sleep(Duration::from_millis(50));
285        handle.send(Message::text("check"));
286        std::thread::sleep(Duration::from_millis(50));
287    }
288
289    #[test]
290    fn named_lookup() {
291        let engine = Engine::new();
292        let h = engine.spawn("logger", |ctx| {
293            ctx.on_message(|msg| println!("{:?}", msg));
294        });
295        std::thread::sleep(Duration::from_millis(10));
296        assert_eq!(engine.lookup("logger"), Some(h.id));
297        engine.send_named("logger", Message::text("hi"));
298        std::thread::sleep(Duration::from_millis(50));
299    }
300
301    #[test]
302    fn broadcast_works() {
303        let engine = Engine::new();
304        let _ = engine.spawn("a", |ctx| {
305            ctx.on_message(|msg| println!("a: {:?}", msg));
306        });
307        let _ = engine.spawn("b", |ctx| {
308            ctx.on_message(|msg| println!("b: {:?}", msg));
309        });
310        std::thread::sleep(Duration::from_millis(20));
311        let sent = engine.broadcast(Message::text("all"));
312        assert_eq!(sent, 2);
313        std::thread::sleep(Duration::from_millis(50));
314    }
315
316    #[test]
317    fn mount_and_unmount() {
318        let engine = Engine::new();
319        let handle = engine.spawn("lifecycle", |ctx| {
320            ctx.on_mount(|| println!("mounted"));
321            ctx.on_unmount(|| println!("unmounted"));
322            ctx.on_message(|_| {});
323        });
324        std::thread::sleep(Duration::from_millis(20));
325        handle.send(Message::text("hi"));
326        std::thread::sleep(Duration::from_millis(50));
327    }
328}