Skip to main content

nautilus_common/actor/
registry.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Thread-local actor registry with lifetime-safe access guards.
17//!
18//! # Design
19//!
20//! The actor registry stores actors in thread-local storage and provides access via
21//! [`ActorRef<T>`] guards. This design addresses several constraints:
22//!
23//! - **Use-after-free prevention**: `ActorRef` holds an `Rc` clone, keeping the actor
24//!   alive even if removed from the registry while the guard exists.
25//! - **Re-entrant callbacks**: Message handlers frequently call back into the registry
26//!   to access other actors. Unlike `RefCell`-style borrow tracking, multiple `ActorRef`
27//!   guards can exist simultaneously without panicking.
28//! - **No `'static` lifetime lie**: Previous designs returned `&'static mut T`, which
29//!   didn't reflect actual validity. The guard-based approach ties the borrow to the
30//!   guard's lifetime.
31//!
32//! # Limitations
33//!
34//! - **Aliasing not prevented**: Two guards can exist for the same actor simultaneously,
35//!   allowing aliased mutable access. This is technically undefined behavior but is
36//!   required by the re-entrant callback pattern. Higher-level discipline is required.
37//! - **Thread-local only**: Guards must not be sent across threads.
38//!
39//! # Invariants
40//!
41//! These contracts must hold regardless of how the registry is implemented
42//! internally. The first three are verified by tests in this module. The
43//! fourth is a usage discipline enforced by convention.
44//!
45//! - **Thread isolation**: Each thread has its own registry instance. An actor
46//!   registered on one thread is never visible from another.
47//! - **Guard survival**: An [`ActorRef`] keeps its actor alive via reference
48//!   counting. Removing or replacing an actor in the registry does not invalidate
49//!   existing guards.
50//! - **Type safety**: [`get_actor_unchecked`] and [`try_get_actor_unchecked`]
51//!   verify the concrete type at runtime before casting. A type mismatch panics
52//!   or returns `None`, respectively.
53//! - **Short-lived guards**: Guards must be obtained, used, and dropped within a
54//!   single synchronous scope. Never store an [`ActorRef`] in a struct or hold
55//!   one across an `.await` point.
56
57use std::{
58    any::TypeId,
59    cell::{RefCell, UnsafeCell},
60    fmt::Debug,
61    marker::PhantomData,
62    ops::{Deref, DerefMut},
63    rc::Rc,
64};
65
66use ahash::AHashMap;
67use ustr::Ustr;
68
69use super::Actor;
70
71/// A guard providing mutable access to an actor.
72///
73/// This guard holds an `Rc` reference to keep the actor alive, preventing
74/// use-after-free if the actor is removed from the registry while the guard
75/// exists. The guard implements `Deref` and `DerefMut` for ergonomic access.
76///
77/// # Safety
78///
79/// While this guard prevents use-after-free from registry removal, it does not
80/// prevent aliasing. Multiple `ActorRef` instances can exist for the same actor
81/// simultaneously, which is technically undefined behavior but is required by
82/// the re-entrant callback pattern in this codebase.
83pub struct ActorRef<T: Actor> {
84    actor_rc: Rc<UnsafeCell<dyn Actor>>,
85    _marker: PhantomData<T>,
86}
87
88impl<T: Actor> Debug for ActorRef<T> {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct(stringify!(ActorRef))
91            .field("actor_id", &self.deref().id())
92            .finish()
93    }
94}
95
96impl<T: Actor> Deref for ActorRef<T> {
97    type Target = T;
98
99    fn deref(&self) -> &Self::Target {
100        // SAFETY: Type was verified at construction time
101        unsafe { &*(self.actor_rc.get() as *const T) }
102    }
103}
104
105impl<T: Actor> DerefMut for ActorRef<T> {
106    fn deref_mut(&mut self) -> &mut Self::Target {
107        // SAFETY: Type was verified at construction time
108        unsafe { &mut *self.actor_rc.get().cast::<T>() }
109    }
110}
111
112thread_local! {
113    static ACTOR_REGISTRY: ActorRegistry = ActorRegistry::new();
114}
115
116/// Registry for storing actors.
117pub struct ActorRegistry {
118    actors: RefCell<AHashMap<Ustr, Rc<UnsafeCell<dyn Actor>>>>,
119}
120
121impl Debug for ActorRegistry {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        let actors_ref = self.actors.borrow();
124        let keys: Vec<&Ustr> = actors_ref.keys().collect();
125        f.debug_struct(stringify!(ActorRegistry))
126            .field("actors", &keys)
127            .finish()
128    }
129}
130
131impl Default for ActorRegistry {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl ActorRegistry {
138    pub fn new() -> Self {
139        Self {
140            actors: RefCell::new(AHashMap::new()),
141        }
142    }
143
144    pub fn insert(&self, id: Ustr, actor: Rc<UnsafeCell<dyn Actor>>) {
145        let mut actors = self.actors.borrow_mut();
146        if actors.contains_key(&id) {
147            log::warn!("Replacing existing actor with id: {id}");
148        }
149        actors.insert(id, actor);
150    }
151
152    pub fn get(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
153        self.actors.borrow().get(id).cloned()
154    }
155
156    /// Returns the number of registered actors.
157    pub fn len(&self) -> usize {
158        self.actors.borrow().len()
159    }
160
161    /// Checks if the registry is empty.
162    pub fn is_empty(&self) -> bool {
163        self.actors.borrow().is_empty()
164    }
165
166    /// Removes an actor from the registry.
167    pub fn remove(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
168        self.actors.borrow_mut().remove(id)
169    }
170
171    /// Checks if an actor with the `id` exists.
172    pub fn contains(&self, id: &Ustr) -> bool {
173        self.actors.borrow().contains_key(id)
174    }
175}
176
177pub fn get_actor_registry() -> &'static ActorRegistry {
178    ACTOR_REGISTRY.with(|registry| unsafe {
179        // SAFETY: We return a static reference that lives for the lifetime of the thread.
180        // Since this is thread_local storage, each thread has its own instance.
181        // The transmute extends the lifetime to 'static which is safe because
182        // thread_local ensures the registry lives for the thread's entire lifetime.
183        std::mem::transmute::<&ActorRegistry, &'static ActorRegistry>(registry)
184    })
185}
186
187/// Registers an actor.
188pub fn register_actor<T>(actor: T) -> Rc<UnsafeCell<T>>
189where
190    T: Actor + 'static,
191{
192    let actor_id = actor.id();
193    let actor_ref = Rc::new(UnsafeCell::new(actor));
194
195    // Register as Actor (message handling only)
196    let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = actor_ref.clone();
197    get_actor_registry().insert(actor_id, actor_trait_ref);
198
199    actor_ref
200}
201
202pub fn get_actor(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Actor>>> {
203    get_actor_registry().get(id)
204}
205
206/// Returns a guard providing mutable access to the registered actor of type `T`.
207///
208/// The returned [`ActorRef`] holds an `Rc` to keep the actor alive, preventing
209/// use-after-free if the actor is removed from the registry.
210///
211/// # Panics
212///
213/// - Panics if no actor with the specified `id` is found in the registry.
214/// - Panics if the stored actor is not of type `T`.
215///
216/// Aliasing constraints apply:
217///
218/// - **Aliasing**: The caller should ensure no other mutable references to the same
219///   actor exist simultaneously. The callback-based message handling pattern in this
220///   codebase requires re-entrant access, which technically violates this invariant.
221/// - **Thread safety**: The registry is thread-local; do not send guards across
222///   threads.
223#[must_use]
224pub fn get_actor_unchecked<T: Actor>(id: &Ustr) -> ActorRef<T> {
225    let registry = get_actor_registry();
226    let actor_rc = registry
227        .get(id)
228        .unwrap_or_else(|| panic!("Actor for {id} not found"));
229
230    // SAFETY: Get a reference to check the type before casting
231    let actor_ref = unsafe { &*actor_rc.get() };
232    let actual_type = actor_ref.as_any().type_id();
233    let expected_type = TypeId::of::<T>();
234
235    assert!(
236        actual_type == expected_type,
237        "Actor type mismatch for '{id}': expected {expected_type:?}, found {actual_type:?}"
238    );
239
240    ActorRef {
241        actor_rc,
242        _marker: PhantomData,
243    }
244}
245
246/// Attempts to get a guard providing mutable access to the registered actor.
247///
248/// Returns `None` if the actor is not found or the type doesn't match.
249///
250/// See [`get_actor_unchecked`] for aliasing and thread-safety constraints.
251#[must_use]
252pub fn try_get_actor_unchecked<T: Actor>(id: &Ustr) -> Option<ActorRef<T>> {
253    let registry = get_actor_registry();
254    let actor_rc = registry.get(id)?;
255
256    // SAFETY: Get a reference to check the type before casting
257    let actor_ref = unsafe { &*actor_rc.get() };
258    let actual_type = actor_ref.as_any().type_id();
259    let expected_type = TypeId::of::<T>();
260
261    if actual_type != expected_type {
262        return None;
263    }
264
265    Some(ActorRef {
266        actor_rc,
267        _marker: PhantomData,
268    })
269}
270
271/// Checks if an actor with the `id` exists in the registry.
272pub fn actor_exists(id: &Ustr) -> bool {
273    get_actor_registry().contains(id)
274}
275
276/// Returns the number of registered actors.
277pub fn actor_count() -> usize {
278    get_actor_registry().len()
279}
280
281#[cfg(test)]
282/// Clears the actor registry (for test isolation).
283pub fn clear_actor_registry() {
284    let registry = get_actor_registry();
285    registry.actors.borrow_mut().clear();
286}
287
288#[cfg(test)]
289mod tests {
290    use std::any::Any;
291
292    use rstest::rstest;
293
294    use super::*;
295
296    #[derive(Debug)]
297    struct TestActor {
298        id: Ustr,
299        value: i32,
300    }
301
302    impl Actor for TestActor {
303        fn id(&self) -> Ustr {
304            self.id
305        }
306        fn handle(&mut self, _msg: &dyn Any) {}
307        fn as_any(&self) -> &dyn Any {
308            self
309        }
310    }
311
312    #[rstest]
313    fn test_register_and_get_actor() {
314        clear_actor_registry();
315
316        let id = Ustr::from("test-actor");
317        let actor = TestActor { id, value: 42 };
318        register_actor(actor);
319
320        let actor_ref = get_actor_unchecked::<TestActor>(&id);
321        assert_eq!(actor_ref.value, 42);
322    }
323
324    #[rstest]
325    fn test_mutation_through_reference() {
326        clear_actor_registry();
327
328        let id = Ustr::from("test-actor-mut");
329        let actor = TestActor { id, value: 0 };
330        register_actor(actor);
331
332        let mut actor_ref = get_actor_unchecked::<TestActor>(&id);
333        actor_ref.value = 999;
334
335        let actor_ref2 = get_actor_unchecked::<TestActor>(&id);
336        assert_eq!(actor_ref2.value, 999);
337    }
338
339    #[rstest]
340    fn test_try_get_returns_none_for_missing() {
341        clear_actor_registry();
342
343        let id = Ustr::from("nonexistent");
344        let result = try_get_actor_unchecked::<TestActor>(&id);
345        assert!(result.is_none());
346    }
347
348    #[rstest]
349    fn test_try_get_returns_none_for_wrong_type() {
350        #[derive(Debug)]
351        struct OtherActor {
352            id: Ustr,
353        }
354
355        impl Actor for OtherActor {
356            fn id(&self) -> Ustr {
357                self.id
358            }
359            fn handle(&mut self, _msg: &dyn Any) {}
360            fn as_any(&self) -> &dyn Any {
361                self
362            }
363        }
364
365        clear_actor_registry();
366
367        let id = Ustr::from("other-actor");
368        let actor = OtherActor { id };
369        register_actor(actor);
370
371        let result = try_get_actor_unchecked::<TestActor>(&id);
372        assert!(result.is_none());
373    }
374
375    #[rstest]
376    fn test_registry_is_thread_local() {
377        clear_actor_registry();
378
379        let id = Ustr::from("thread-local-actor");
380        let actor = TestActor { id, value: 42 };
381        register_actor(actor);
382
383        assert!(actor_exists(&id));
384        assert_eq!(actor_count(), 1);
385
386        let visible_on_other_thread = std::thread::spawn(move || {
387            // Each thread gets its own empty registry
388            (actor_exists(&id), actor_count())
389        })
390        .join()
391        .unwrap();
392
393        assert!(!visible_on_other_thread.0);
394        assert_eq!(visible_on_other_thread.1, 0);
395    }
396
397    #[rstest]
398    fn test_actor_ref_survives_registry_removal() {
399        clear_actor_registry();
400
401        let id = Ustr::from("removable-actor");
402        let actor = TestActor { id, value: 7 };
403        register_actor(actor);
404        assert_eq!(actor_count(), 1);
405
406        let mut guard = get_actor_unchecked::<TestActor>(&id);
407
408        get_actor_registry().remove(&id);
409        assert!(!actor_exists(&id));
410        assert_eq!(actor_count(), 0);
411
412        assert_eq!(guard.value, 7);
413        guard.value = 99;
414        assert_eq!(guard.value, 99);
415    }
416
417    #[rstest]
418    fn test_actor_ref_survives_same_id_replacement() {
419        clear_actor_registry();
420
421        let id = Ustr::from("replaceable-actor");
422        let actor_a = TestActor { id, value: 1 };
423        register_actor(actor_a);
424
425        let guard_a = get_actor_unchecked::<TestActor>(&id);
426        assert_eq!(guard_a.value, 1);
427
428        let actor_b = TestActor { id, value: 2 };
429        register_actor(actor_b);
430
431        // Old guard still sees actor A
432        assert_eq!(guard_a.value, 1);
433
434        // Fresh lookup sees actor B
435        let guard_b = get_actor_unchecked::<TestActor>(&id);
436        assert_eq!(guard_b.value, 2);
437        assert_eq!(actor_count(), 1);
438    }
439
440    #[should_panic(expected = "Actor type mismatch")]
441    #[rstest]
442    fn test_get_actor_unchecked_panics_on_type_mismatch() {
443        #[derive(Debug)]
444        struct OtherActor {
445            id: Ustr,
446        }
447
448        impl Actor for OtherActor {
449            fn id(&self) -> Ustr {
450                self.id
451            }
452            fn handle(&mut self, _msg: &dyn Any) {}
453            fn as_any(&self) -> &dyn Any {
454                self
455            }
456        }
457
458        clear_actor_registry();
459
460        let id = Ustr::from("typed-actor");
461        let actor = OtherActor { id };
462        register_actor(actor);
463
464        let _guard = get_actor_unchecked::<TestActor>(&id);
465    }
466}