1use dashmap::DashMap;
6use parking_lot::RwLock;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::Error;
11use crate::{ActorId, Mailbox, MailboxConfig};
12
13fn unix_now() -> i64 {
15 std::time::SystemTime::now()
16 .duration_since(std::time::UNIX_EPOCH)
17 .map(|d| d.as_secs() as i64)
18 .unwrap_or(0)
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub enum ActorState {
24 #[default]
26 Creating,
27 Running,
29 Suspended,
31 Stopped,
33 Failed,
35}
36
37struct ActorEntry {
39 name: Option<String>,
41 state: std::sync::atomic::AtomicU8,
43 mailbox: Arc<Mailbox>,
45 processed: std::sync::atomic::AtomicU64,
47 last_active: std::sync::atomic::AtomicI64,
49}
50
51impl ActorState {
52 pub fn to_u8(self) -> u8 {
54 match self {
55 ActorState::Creating => 0,
56 ActorState::Running => 1,
57 ActorState::Suspended => 2,
58 ActorState::Stopped => 3,
59 ActorState::Failed => 4,
60 }
61 }
62
63 pub fn from_u8(v: u8) -> Self {
66 match v {
67 0 => ActorState::Creating,
68 1 => ActorState::Running,
69 2 => ActorState::Suspended,
70 3 => ActorState::Stopped,
71 4 => ActorState::Failed,
72 _ => ActorState::Creating,
73 }
74 }
75}
76
77pub struct ActorRegistry {
79 by_id: DashMap<ActorId, Arc<ActorEntry>>,
81 by_name: RwLock<HashMap<String, ActorId>>,
83 mailbox_config: MailboxConfig,
85}
86
87impl ActorRegistry {
88 pub fn new() -> Self {
90 Self {
91 by_id: DashMap::new(),
92 by_name: RwLock::new(HashMap::new()),
93 mailbox_config: MailboxConfig::default(),
94 }
95 }
96
97 pub fn with_mailbox_config(config: MailboxConfig) -> Self {
99 Self {
100 by_id: DashMap::new(),
101 by_name: RwLock::new(HashMap::new()),
102 mailbox_config: config,
103 }
104 }
105
106 pub fn register(&self, id: ActorId) -> crate::Result<Arc<Mailbox>> {
108 self.register_named(id, None)
109 }
110
111 pub fn register_named(&self, id: ActorId, name: Option<String>) -> crate::Result<Arc<Mailbox>> {
118 let mailbox = Arc::new(Mailbox::new(id, self.mailbox_config.clone()));
119
120 let entry = Arc::new(ActorEntry {
121 name: name.clone(),
122 state: std::sync::atomic::AtomicU8::new(ActorState::Creating.to_u8()),
123 mailbox: mailbox.clone(),
124 processed: std::sync::atomic::AtomicU64::new(0),
125 last_active: std::sync::atomic::AtomicI64::new(unix_now()),
126 });
127
128 let mut by_name = self.by_name.write();
133
134 if self.by_id.contains_key(&id) {
136 return Err(Error::actor(format!(
137 "actor with id {:?} already exists",
138 id
139 )));
140 }
141
142 if let Some(ref actor_name) = name {
144 if by_name.contains_key(actor_name) {
145 return Err(Error::actor(format!(
146 "actor with name '{}' already exists",
147 actor_name
148 )));
149 }
150 }
151
152 if self.by_id.insert(id, entry).is_some() {
155 return Err(Error::actor(format!(
156 "actor with id {:?} already exists",
157 id
158 )));
159 }
160
161 if let Some(ref actor_name) = name {
163 by_name.insert(actor_name.clone(), id);
164 }
165
166 Ok(mailbox)
168 }
169
170 pub fn unregister(&self, id: &ActorId) -> crate::Result<()> {
172 if let Some((_, entry)) = self.by_id.remove(id) {
173 if let Some(name) = &entry.name {
174 self.by_name.write().remove(name);
175 }
176 Ok(())
177 } else {
178 Err(Error::actor(format!("actor {:?} not found", id)))
179 }
180 }
181
182 pub fn get_mailbox(&self, id: &ActorId) -> Option<Arc<Mailbox>> {
184 self.by_id.get(id).map(|e| e.mailbox.clone())
185 }
186
187 pub fn get_state(&self, id: &ActorId) -> Option<ActorState> {
189 self.by_id
190 .get(id)
191 .map(|e| ActorState::from_u8(e.state.load(std::sync::atomic::Ordering::Relaxed)))
192 }
193
194 pub fn set_state(&self, id: &ActorId, state: ActorState) -> crate::Result<()> {
196 if let Some(entry) = self.by_id.get(id) {
197 entry
198 .state
199 .store(state.to_u8(), std::sync::atomic::Ordering::Relaxed);
200 Ok(())
201 } else {
202 Err(Error::actor(format!("actor {:?} not found", id)))
203 }
204 }
205
206 pub fn lookup(&self, name: &str) -> Option<ActorId> {
208 self.by_name.read().get(name).copied()
209 }
210
211 pub fn lookup_mailbox(&self, name: &str) -> Option<Arc<Mailbox>> {
213 let id = self.lookup(name)?;
214 self.get_mailbox(&id)
215 }
216
217 pub fn record_processed(&self, id: &ActorId) {
219 if let Some(entry) = self.by_id.get(id) {
220 entry
221 .processed
222 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
223 entry
224 .last_active
225 .store(unix_now(), std::sync::atomic::Ordering::Relaxed);
226 }
227 }
228
229 pub fn get_processed_count(&self, id: &ActorId) -> u64 {
231 self.by_id
232 .get(id)
233 .map(|e| e.processed.load(std::sync::atomic::Ordering::Relaxed))
234 .unwrap_or(0)
235 }
236
237 pub fn get_last_active(&self, id: &ActorId) -> Option<i64> {
239 self.by_id
240 .get(id)
241 .map(|e| e.last_active.load(std::sync::atomic::Ordering::Relaxed))
242 }
243
244 pub fn len(&self) -> usize {
246 self.by_id.len()
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.by_id.is_empty()
252 }
253
254 pub fn list_actors(&self) -> Vec<ActorId> {
256 self.by_id.iter().map(|e| *e.key()).collect()
257 }
258
259 pub fn list_by_state(&self, state: ActorState) -> Vec<ActorId> {
261 let target = state.to_u8();
262 self.by_id
263 .iter()
264 .filter(|e| e.state.load(std::sync::atomic::Ordering::Relaxed) == target)
265 .map(|e| *e.key())
266 .collect()
267 }
268
269 pub fn stats(&self) -> RegistryStats {
271 let mut stats = RegistryStats::default();
272
273 for entry in self.by_id.iter() {
274 let state = ActorState::from_u8(entry.state.load(std::sync::atomic::Ordering::Relaxed));
275 match state {
276 ActorState::Creating => stats.creating += 1,
277 ActorState::Running => stats.running += 1,
278 ActorState::Suspended => stats.suspended += 1,
279 ActorState::Stopped => stats.stopped += 1,
280 ActorState::Failed => stats.failed += 1,
281 }
282 }
283 stats.total = self.by_id.len();
284 stats
285 }
286}
287
288impl Default for ActorRegistry {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[derive(Debug, Default)]
296pub struct RegistryStats {
297 pub total: usize,
299 pub creating: usize,
301 pub running: usize,
303 pub suspended: usize,
305 pub stopped: usize,
307 pub failed: usize,
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_registry_basic() {
317 let registry = ActorRegistry::new();
318 let id = ActorId::new();
319
320 let _mailbox = registry.register(id).unwrap();
321 assert_eq!(registry.len(), 1);
322 assert_eq!(registry.get_state(&id), Some(ActorState::Creating));
323
324 registry.set_state(&id, ActorState::Running).unwrap();
325 assert_eq!(registry.get_state(&id), Some(ActorState::Running));
326
327 registry.unregister(&id).unwrap();
328 assert!(registry.is_empty());
329 }
330
331 #[test]
332 fn test_registry_named() {
333 let registry = ActorRegistry::new();
334 let id = ActorId::new();
335
336 registry
337 .register_named(id, Some("test-actor".to_string()))
338 .unwrap();
339
340 let looked_up = registry.lookup("test-actor");
341 assert_eq!(looked_up, Some(id));
342
343 let mailbox = registry.lookup_mailbox("test-actor");
344 assert!(mailbox.is_some());
345 }
346
347 #[test]
348 fn test_registry_duplicate_name() {
349 let registry = ActorRegistry::new();
350
351 registry
352 .register_named(ActorId::new(), Some("test".to_string()))
353 .unwrap();
354
355 let result = registry.register_named(ActorId::new(), Some("test".to_string()));
356 assert!(result.is_err());
357 }
358
359 #[test]
360 fn test_registry_stats() {
361 let registry = ActorRegistry::new();
362
363 let id1 = ActorId::new();
364 let id2 = ActorId::new();
365 let id3 = ActorId::new();
366
367 registry.register(id1).unwrap();
368 registry.register(id2).unwrap();
369 registry.register(id3).unwrap();
370
371 registry.set_state(&id1, ActorState::Running).unwrap();
372 registry.set_state(&id2, ActorState::Running).unwrap();
373 registry.set_state(&id3, ActorState::Suspended).unwrap();
374
375 let stats = registry.stats();
376 assert_eq!(stats.total, 3);
377 assert_eq!(stats.running, 2);
378 assert_eq!(stats.suspended, 1);
379 }
380
381 #[test]
382 fn test_registry_concurrent_named_registration() {
383 use std::sync::Arc;
384 use std::thread;
385
386 let registry = Arc::new(ActorRegistry::new());
387 let mut handles = Vec::new();
388
389 for _ in 0..16 {
392 let reg = registry.clone();
393 handles.push(thread::spawn(move || {
394 let id = ActorId::new();
395 reg.register_named(id, Some("shared-name".to_string()))
396 }));
397 }
398
399 let mut success_count = 0;
400 let mut error_count = 0;
401 for handle in handles {
402 match handle.join().unwrap() {
403 Ok(_) => success_count += 1,
404 Err(_) => error_count += 1,
405 }
406 }
407
408 assert_eq!(
409 success_count, 1,
410 "exactly one registration with shared name should succeed"
411 );
412 assert_eq!(error_count, 15, "all other registrations should fail");
413 assert_eq!(registry.len(), 1);
414 }
415
416 #[test]
417 fn test_registry_no_orphaned_name_on_id_conflict() {
418 let registry = ActorRegistry::new();
421 let id = ActorId::new();
422
423 registry
425 .register_named(id, Some("my-actor".to_string()))
426 .unwrap();
427
428 let result = registry.register_named(ActorId::new(), Some("my-actor".to_string()));
430 assert!(result.is_err());
431
432 assert_eq!(registry.lookup("my-actor"), Some(id));
434 }
435
436 #[test]
437 fn test_registry_toctou_unnamed_vs_named_same_id() {
438 use std::sync::atomic::{AtomicUsize, Ordering};
443 use std::thread;
444
445 let registry = Arc::new(ActorRegistry::new());
446 let id = ActorId::new();
447 let success_count = Arc::new(AtomicUsize::new(0));
448
449 let named_id = id;
451 let unnamed_id = id;
452 let reg1 = registry.clone();
453 let reg2 = registry.clone();
454 let sc1 = success_count.clone();
455 let sc2 = success_count.clone();
456
457 let h1 = thread::spawn(move || {
458 if reg1
459 .register_named(named_id, Some("named-actor".to_string()))
460 .is_ok()
461 {
462 sc1.fetch_add(1, Ordering::Relaxed);
463 }
464 });
465
466 let h2 = thread::spawn(move || {
467 if reg2.register_named(unnamed_id, None).is_ok() {
468 sc2.fetch_add(1, Ordering::Relaxed);
469 }
470 });
471
472 h1.join().unwrap();
473 h2.join().unwrap();
474
475 assert_eq!(
476 success_count.load(Ordering::Relaxed),
477 1,
478 "exactly one registration with the same ID should succeed"
479 );
480 assert_eq!(registry.len(), 1);
481 }
482}