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 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}