Skip to main content

nautilus_common/
component.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//! Component system for managing stateful system entities.
17//!
18//! This module provides the component framework for managing the lifecycle and state
19//! of system entities. Components have defined states (pre-initialized, ready, running,
20//! stopped, etc.) and provide a consistent interface for state management and transitions.
21
22#![allow(unsafe_code)]
23
24use std::{
25    cell::{RefCell, UnsafeCell},
26    fmt::Debug,
27    rc::Rc,
28};
29
30use ahash::{AHashMap, AHashSet};
31use nautilus_model::identifiers::{ComponentId, TraderId};
32use thiserror::Error;
33use ustr::Ustr;
34
35use crate::{
36    actor::{Actor, registry::with_actor_registry},
37    cache::Cache,
38    clock::Clock,
39    enums::{ComponentState, ComponentTrigger},
40};
41
42/// Failure to acquire access to component state.
43///
44/// A conflict identifies the requested access, not the holder or its call stack.
45/// Callback reentry can cause a conflict, but is not the only possible cause.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
47pub enum ComponentAccessError {
48    /// Registration has not supplied the requested resource.
49    #[error("Cannot access {resource} during {operation}: the actor is not registered")]
50    NotRegistered {
51        /// The resource being accessed.
52        resource: &'static str,
53        /// The attempted operation.
54        operation: &'static str,
55    },
56    /// Shared access conflicts with an existing exclusive borrow.
57    #[error(
58        "Cannot read {resource} during {operation}: it is already mutably borrowed. Release the existing borrow before accessing it again; callback reentry can cause this conflict"
59    )]
60    ReadConflict {
61        /// The resource being accessed.
62        resource: &'static str,
63        /// The attempted operation.
64        operation: &'static str,
65    },
66    /// Exclusive access conflicts with an existing shared or exclusive borrow.
67    #[error(
68        "Cannot modify {resource} during {operation}: it is already borrowed. Release existing borrows before accessing it mutably; callback reentry can cause this conflict"
69    )]
70    WriteConflict {
71        /// The resource being accessed.
72        resource: &'static str,
73        /// The attempted operation.
74        operation: &'static str,
75    },
76}
77
78/// Components have state and lifecycle management capabilities.
79pub trait Component {
80    /// Returns the unique identifier for this component.
81    fn component_id(&self) -> ComponentId;
82
83    /// Returns the current state of the component.
84    fn state(&self) -> ComponentState;
85
86    /// Transition the component with the state trigger.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the `trigger` is an invalid transition from the current state.
91    fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()>;
92
93    /// Returns whether the component is ready.
94    fn is_ready(&self) -> bool {
95        self.state() == ComponentState::Ready
96    }
97
98    /// Returns whether the component is *not* running.
99    fn not_running(&self) -> bool {
100        !self.is_running()
101    }
102
103    /// Returns whether the component is running.
104    fn is_running(&self) -> bool {
105        self.state() == ComponentState::Running
106    }
107
108    /// Returns whether the component is stopped.
109    fn is_stopped(&self) -> bool {
110        self.state() == ComponentState::Stopped
111    }
112
113    /// Returns whether the component has been degraded.
114    fn is_degraded(&self) -> bool {
115        self.state() == ComponentState::Degraded
116    }
117
118    /// Returns whether the component has been faulted.
119    fn is_faulted(&self) -> bool {
120        self.state() == ComponentState::Faulted
121    }
122
123    /// Returns whether the component has been disposed.
124    fn is_disposed(&self) -> bool {
125        self.state() == ComponentState::Disposed
126    }
127
128    /// Registers the component with a system.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the component fails to register.
133    fn register(
134        &mut self,
135        trader_id: TraderId,
136        clock: Rc<RefCell<dyn Clock>>,
137        cache: Rc<RefCell<Cache>>,
138    ) -> anyhow::Result<()>;
139
140    /// Initializes the component.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the initialization state transition fails.
145    fn initialize(&mut self) -> anyhow::Result<()> {
146        self.transition_state(ComponentTrigger::Initialize)
147    }
148
149    /// Starts the component.
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if the component fails to start.
154    fn start(&mut self) -> anyhow::Result<()> {
155        self.transition_state(ComponentTrigger::Start)?; // -> Starting
156
157        if let Err(e) = self.on_start() {
158            log_error(self.component_id(), &e);
159            return Err(e); // Halt state transition
160        }
161
162        self.transition_state(ComponentTrigger::StartCompleted)?;
163
164        Ok(())
165    }
166
167    /// Stops the component.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if the component fails to stop.
172    fn stop(&mut self) -> anyhow::Result<()> {
173        self.transition_state(ComponentTrigger::Stop)?; // -> Stopping
174
175        if let Err(e) = self.on_stop() {
176            log_error(self.component_id(), &e);
177            return Err(e); // Halt state transition
178        }
179
180        self.transition_state(ComponentTrigger::StopCompleted)?;
181
182        Ok(())
183    }
184
185    /// Resumes the component.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the component fails to resume.
190    fn resume(&mut self) -> anyhow::Result<()> {
191        self.transition_state(ComponentTrigger::Resume)?; // -> Resuming
192
193        if let Err(e) = self.on_resume() {
194            log_error(self.component_id(), &e);
195            return Err(e); // Halt state transition
196        }
197
198        self.transition_state(ComponentTrigger::ResumeCompleted)?;
199
200        Ok(())
201    }
202
203    /// Degrades the component.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the component fails to degrade.
208    fn degrade(&mut self) -> anyhow::Result<()> {
209        self.transition_state(ComponentTrigger::Degrade)?; // -> Degrading
210
211        if let Err(e) = self.on_degrade() {
212            log_error(self.component_id(), &e);
213            return Err(e); // Halt state transition
214        }
215
216        self.transition_state(ComponentTrigger::DegradeCompleted)?;
217
218        Ok(())
219    }
220
221    /// Faults the component.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if the component fails to fault.
226    ///
227    /// # Notes
228    ///
229    /// Subscriptions are released whether or not `on_fault` succeeds. This applies to faults
230    /// initiated through this method; a failed `on_dispose` reaches `Faulted` without invoking
231    /// `on_fault` and retains subscriptions until a later retirement.
232    fn fault(&mut self) -> anyhow::Result<()> {
233        self.transition_state(ComponentTrigger::Fault)?; // -> Faulting
234
235        let result = self.on_fault();
236        self.release_subscriptions();
237
238        if let Err(e) = result {
239            log_error(self.component_id(), &e);
240            return Err(e); // Halt state transition
241        }
242
243        self.transition_state(ComponentTrigger::FaultCompleted)?;
244
245        Ok(())
246    }
247
248    /// Resets the component to its initial state.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the component fails to reset.
253    ///
254    /// # Notes
255    ///
256    /// A successful reset releases retained subscriptions so the component can acquire fresh
257    /// subscriptions when it next starts. A failing `on_reset` retains subscriptions and leaves
258    /// the component in `Resetting`.
259    fn reset(&mut self) -> anyhow::Result<()> {
260        self.transition_state(ComponentTrigger::Reset)?; // -> Resetting
261
262        if let Err(e) = self.on_reset() {
263            log_error(self.component_id(), &e);
264            return Err(e); // Halt state transition
265        }
266
267        self.release_subscriptions();
268        self.transition_state(ComponentTrigger::ResetCompleted)?;
269
270        Ok(())
271    }
272
273    /// Disposes of the component, releasing any resources.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the component fails to dispose.
278    ///
279    /// # Notes
280    ///
281    /// A failing `on_dispose` moves the component to `Faulted` and returns the error without
282    /// releasing subscriptions. The trader keeps its registry entries, clock, bookkeeping, and
283    /// retained Python wrapper, if any, so the failed retirement leaves the component reachable
284    /// and can be retried without a partially dismantled registration.
285    ///
286    /// `on_fault` does not run, since invoking a second user hook immediately after `on_dispose`
287    /// failed can fail again.
288    fn dispose(&mut self) -> anyhow::Result<()> {
289        self.transition_state(ComponentTrigger::Dispose)?; // -> Disposing
290
291        if let Err(e) = self.on_dispose() {
292            log_error(self.component_id(), &e);
293
294            self.transition_state(ComponentTrigger::Fault)?; // -> Faulting
295            self.transition_state(ComponentTrigger::FaultCompleted)?; // -> Faulted
296
297            return Err(e);
298        }
299
300        self.release_subscriptions();
301        self.transition_state(ComponentTrigger::DisposeCompleted)?;
302
303        Ok(())
304    }
305
306    /// Releases the message bus registrations this component installed.
307    ///
308    /// Runs after successful `on_reset` and `on_dispose` hooks, after `on_fault` returns, and during
309    /// explicit retirement cleanup. An override must handle every route and be idempotent so a
310    /// failed disposal can release its subscriptions during a later retirement.
311    fn release_subscriptions(&mut self) {}
312
313    /// Actions to be performed on start.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if starting the actor fails.
318    fn on_start(&mut self) -> anyhow::Result<()> {
319        log::warn!(
320            "The `on_start` handler was called when not overridden, \
321            it's expected that any actions required when stopping the component \
322            occur here, such as unsubscribing from data",
323        );
324        Ok(())
325    }
326
327    /// Actions to be performed on stop.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if stopping the actor fails.
332    fn on_stop(&mut self) -> anyhow::Result<()> {
333        log::warn!(
334            "The `on_stop` handler was called when not overridden, \
335            it's expected that any actions required when stopping the component \
336            occur here, such as unsubscribing from data",
337        );
338        Ok(())
339    }
340
341    /// Actions to be performed on resume.
342    ///
343    /// # Errors
344    ///
345    /// Returns an error if resuming the actor fails.
346    fn on_resume(&mut self) -> anyhow::Result<()> {
347        log::warn!(
348            "The `on_resume` handler was called when not overridden, \
349            it's expected that any actions required when resuming the component \
350            following a stop occur here"
351        );
352        Ok(())
353    }
354
355    /// Actions to be performed on reset.
356    ///
357    /// # Errors
358    ///
359    /// Returns an error if resetting the actor fails.
360    fn on_reset(&mut self) -> anyhow::Result<()> {
361        log::warn!(
362            "The `on_reset` handler was called when not overridden, \
363            it's expected that any actions required when resetting the component \
364            occur here, such as resetting indicators and other state"
365        );
366        Ok(())
367    }
368
369    /// Actions to be performed on dispose.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if disposing the actor fails.
374    fn on_dispose(&mut self) -> anyhow::Result<()> {
375        Ok(())
376    }
377
378    /// Actions to be performed on degrade.
379    ///
380    /// # Errors
381    ///
382    /// Returns an error if degrading the actor fails.
383    fn on_degrade(&mut self) -> anyhow::Result<()> {
384        Ok(())
385    }
386
387    /// Actions to be performed on fault.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if faulting the actor fails.
392    fn on_fault(&mut self) -> anyhow::Result<()> {
393        Ok(())
394    }
395}
396
397fn log_error(component: ComponentId, e: &anyhow::Error) {
398    log::error!(component = component.as_str(); "{e}");
399}
400
401#[rustfmt::skip]
402impl ComponentState {
403    /// Transition the state machine with the component `trigger`.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if `trigger` is invalid for the current state.
408    pub fn transition(&mut self, trigger: &ComponentTrigger) -> anyhow::Result<Self> {
409        let new_state = match (&self, trigger) {
410            (Self::PreInitialized, ComponentTrigger::Initialize) => Self::Ready,
411            (Self::Ready, ComponentTrigger::Reset) => Self::Resetting,
412            (Self::Ready, ComponentTrigger::Start) => Self::Starting,
413            (Self::Ready, ComponentTrigger::Dispose) => Self::Disposing,
414            (Self::Resetting, ComponentTrigger::ResetCompleted) => Self::Ready,
415            (Self::Starting, ComponentTrigger::StartCompleted) => Self::Running,
416            (Self::Starting, ComponentTrigger::Stop) => Self::Stopping,
417            (Self::Starting, ComponentTrigger::Fault) => Self::Faulting,
418            (Self::Running, ComponentTrigger::Stop) => Self::Stopping,
419            (Self::Running, ComponentTrigger::Degrade) => Self::Degrading,
420            (Self::Running, ComponentTrigger::Fault) => Self::Faulting,
421            (Self::Resuming, ComponentTrigger::Stop) => Self::Stopping,
422            (Self::Resuming, ComponentTrigger::ResumeCompleted) => Self::Running,
423            (Self::Resuming, ComponentTrigger::Fault) => Self::Faulting,
424            (Self::Stopping, ComponentTrigger::StopCompleted) => Self::Stopped,
425            (Self::Stopping, ComponentTrigger::Dispose) => Self::Disposing,
426            (Self::Stopping, ComponentTrigger::Fault) => Self::Faulting,
427            (Self::Stopped, ComponentTrigger::Reset) => Self::Resetting,
428            (Self::Stopped, ComponentTrigger::Resume) => Self::Resuming,
429            (Self::Stopped, ComponentTrigger::Dispose) => Self::Disposing,
430            (Self::Stopped, ComponentTrigger::Fault) => Self::Faulting,
431            (Self::Degrading, ComponentTrigger::DegradeCompleted) => Self::Degraded,
432            (Self::Degraded, ComponentTrigger::Resume) => Self::Resuming,
433            (Self::Degraded, ComponentTrigger::Stop) => Self::Stopping,
434            (Self::Degraded, ComponentTrigger::Fault) => Self::Faulting,
435            (Self::Disposing, ComponentTrigger::DisposeCompleted) => Self::Disposed,
436            (Self::Disposing, ComponentTrigger::Fault) => Self::Faulting,
437            (Self::Faulting, ComponentTrigger::Dispose) => Self::Disposing,
438            (Self::Faulting, ComponentTrigger::FaultCompleted) => Self::Faulted,
439            _ => anyhow::bail!("Invalid state trigger {self} -> {trigger}"),
440        };
441        Ok(new_state)
442    }
443}
444
445thread_local! {
446    static COMPONENT_REGISTRY: ComponentRegistry = ComponentRegistry::new();
447}
448
449/// Registry for storing components with runtime borrow tracking.
450///
451/// The registry tracks which components are currently mutably borrowed to prevent
452/// multiple simultaneous mutable borrows (which would be undefined behavior).
453pub struct ComponentRegistry {
454    components: RefCell<AHashMap<Ustr, Rc<UnsafeCell<dyn Component>>>>,
455    borrows: RefCell<AHashSet<Ustr>>,
456}
457
458impl Debug for ComponentRegistry {
459    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460        let components_ref = self.components.borrow();
461        let keys: Vec<&Ustr> = components_ref.keys().collect();
462        f.debug_struct(stringify!(ComponentRegistry))
463            .field("components", &keys)
464            .field("active_borrows", &self.borrows.borrow().len())
465            .finish()
466    }
467}
468
469impl Default for ComponentRegistry {
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475impl ComponentRegistry {
476    pub fn new() -> Self {
477        Self {
478            components: RefCell::new(AHashMap::new()),
479            borrows: RefCell::new(AHashSet::new()),
480        }
481    }
482
483    pub fn insert(&self, id: Ustr, component: Rc<UnsafeCell<dyn Component>>) {
484        self.components.borrow_mut().insert(id, component);
485    }
486
487    pub fn get(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
488        self.components.borrow().get(id).cloned()
489    }
490
491    /// Removes the component with `id`, returning it when it was registered.
492    pub fn remove(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
493        self.components.borrow_mut().remove(id)
494    }
495
496    /// Checks if a component is currently borrowed.
497    pub fn is_borrowed(&self, id: &Ustr) -> bool {
498        self.borrows.borrow().contains(id)
499    }
500
501    /// Marks a component as borrowed. Returns false if already borrowed.
502    fn try_borrow(&self, id: Ustr) -> bool {
503        let mut borrows = self.borrows.borrow_mut();
504        if borrows.contains(&id) {
505            false
506        } else {
507            borrows.insert(id);
508            true
509        }
510    }
511
512    /// Releases a borrow on a component.
513    fn release_borrow(&self, id: &Ustr) {
514        self.borrows.borrow_mut().remove(id);
515    }
516}
517
518/// Guard that releases a component borrow when dropped.
519///
520/// This ensures borrows are released even if the code panics during
521/// a lifecycle method call.
522struct BorrowGuard {
523    id: Ustr,
524}
525
526impl BorrowGuard {
527    fn new(id: Ustr) -> Self {
528        Self { id }
529    }
530}
531
532impl Drop for BorrowGuard {
533    fn drop(&mut self) {
534        with_component_registry(|registry| registry.release_borrow(&self.id));
535    }
536}
537
538pub fn with_component_registry<R>(f: impl FnOnce(&ComponentRegistry) -> R) -> R {
539    COMPONENT_REGISTRY.with(f)
540}
541
542/// Registers a component.
543pub fn register_component<T>(component: T) -> Rc<UnsafeCell<T>>
544where
545    T: Component + 'static,
546{
547    let component_id = component.component_id().inner();
548    let component_ref = Rc::new(UnsafeCell::new(component));
549
550    // Register in component registry
551    let component_trait_ref: Rc<UnsafeCell<dyn Component>> = component_ref.clone();
552    with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
553
554    component_ref
555}
556
557/// Registers a component that also implements Actor.
558pub fn register_component_actor<T>(component: T) -> Rc<UnsafeCell<T>>
559where
560    T: Component + Actor + 'static,
561{
562    let component_id = component.component_id().inner();
563    let actor_id = component.id();
564    let component_ref = Rc::new(UnsafeCell::new(component));
565
566    // Register in component registry
567    let component_trait_ref: Rc<UnsafeCell<dyn Component>> = component_ref.clone();
568    with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
569
570    // Register in actor registry
571    let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = component_ref.clone();
572    with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
573
574    component_ref
575}
576
577/// Safely calls `start()` on a component in the global registry.
578///
579/// # Errors
580///
581/// - Returns an error if the component is not found.
582/// - Returns an error if the component is already borrowed.
583/// - Returns an error if `start()` fails.
584pub fn start_component(id: &Ustr) -> anyhow::Result<()> {
585    let component_ref = with_component_registry(|registry| {
586        let component_ref = registry
587            .get(id)
588            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
589
590        if !registry.try_borrow(*id) {
591            anyhow::bail!(
592                "Component '{id}' is already mutably borrowed. \
593                 This would create aliasing mutable references (undefined behavior)."
594            );
595        }
596
597        Ok::<_, anyhow::Error>(component_ref)
598    })?;
599
600    let _guard = BorrowGuard::new(*id);
601
602    // SAFETY: Borrow tracking ensures exclusive access
603    unsafe {
604        let component = &mut *component_ref.get();
605        component.start()
606    }
607}
608
609/// Returns the state of a component in the global registry.
610///
611/// # Errors
612///
613/// - Returns an error if the component is not found.
614/// - Returns an error if the component is already borrowed.
615pub fn component_state(id: &Ustr) -> anyhow::Result<ComponentState> {
616    let component_ref = with_component_registry(|registry| {
617        let component_ref = registry
618            .get(id)
619            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
620
621        if !registry.try_borrow(*id) {
622            anyhow::bail!(
623                "Component '{id}' is already mutably borrowed. \
624                 This would create aliasing mutable references (undefined behavior)."
625            );
626        }
627
628        Ok::<_, anyhow::Error>(component_ref)
629    })?;
630
631    let _guard = BorrowGuard::new(*id);
632
633    // SAFETY: Borrow tracking ensures there is no concurrent mutable lifecycle access.
634    unsafe {
635        let component = &*component_ref.get();
636        Ok(component.state())
637    }
638}
639
640/// Safely calls `stop()` on a component in the global registry.
641///
642/// # Errors
643///
644/// - Returns an error if the component is not found.
645/// - Returns an error if the component is already borrowed.
646/// - Returns an error if `stop()` fails.
647pub fn stop_component(id: &Ustr) -> anyhow::Result<()> {
648    let component_ref = with_component_registry(|registry| {
649        let component_ref = registry
650            .get(id)
651            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
652
653        if !registry.try_borrow(*id) {
654            anyhow::bail!(
655                "Component '{id}' is already mutably borrowed. \
656                 This would create aliasing mutable references (undefined behavior)."
657            );
658        }
659
660        Ok::<_, anyhow::Error>(component_ref)
661    })?;
662
663    let _guard = BorrowGuard::new(*id);
664
665    // SAFETY: Borrow tracking ensures exclusive access
666    unsafe {
667        let component = &mut *component_ref.get();
668        component.stop()
669    }
670}
671
672/// Safely calls `reset()` on a component in the global registry.
673///
674/// # Errors
675///
676/// - Returns an error if the component is not found.
677/// - Returns an error if the component is already borrowed.
678/// - Returns an error if `reset()` fails.
679pub fn reset_component(id: &Ustr) -> anyhow::Result<()> {
680    let component_ref = with_component_registry(|registry| {
681        let component_ref = registry
682            .get(id)
683            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
684
685        if !registry.try_borrow(*id) {
686            anyhow::bail!(
687                "Component '{id}' is already mutably borrowed. \
688                 This would create aliasing mutable references (undefined behavior)."
689            );
690        }
691
692        Ok::<_, anyhow::Error>(component_ref)
693    })?;
694
695    let _guard = BorrowGuard::new(*id);
696
697    // SAFETY: Borrow tracking ensures exclusive access
698    unsafe {
699        let component = &mut *component_ref.get();
700        component.reset()
701    }
702}
703
704/// Safely calls `dispose()` on a component in the global registry.
705///
706/// # Errors
707///
708/// - Returns an error if the component is not found.
709/// - Returns an error if the component is already borrowed.
710/// - Returns an error if `dispose()` fails.
711pub fn dispose_component(id: &Ustr) -> anyhow::Result<()> {
712    let component_ref = with_component_registry(|registry| {
713        let component_ref = registry
714            .get(id)
715            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
716
717        if !registry.try_borrow(*id) {
718            anyhow::bail!(
719                "Component '{id}' is already mutably borrowed. \
720                 This would create aliasing mutable references (undefined behavior)."
721            );
722        }
723
724        Ok::<_, anyhow::Error>(component_ref)
725    })?;
726
727    let _guard = BorrowGuard::new(*id);
728
729    // SAFETY: Borrow tracking ensures exclusive access
730    unsafe {
731        let component = &mut *component_ref.get();
732        component.dispose()
733    }
734}
735
736/// Releases subscriptions for a component in the global registry.
737///
738/// This is used when retiring a component whose earlier `on_dispose` failed after the framework
739/// left its registration intact.
740///
741/// # Errors
742///
743/// - Returns an error if the component is not found.
744/// - Returns an error if the component is already borrowed.
745pub fn release_component_subscriptions(id: &Ustr) -> anyhow::Result<()> {
746    let component_ref = with_component_registry(|registry| {
747        let component_ref = registry
748            .get(id)
749            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
750
751        if !registry.try_borrow(*id) {
752            anyhow::bail!(
753                "Component '{id}' is already mutably borrowed. \
754                 This would create aliasing mutable references (undefined behavior)."
755            );
756        }
757
758        Ok::<_, anyhow::Error>(component_ref)
759    })?;
760
761    let _guard = BorrowGuard::new(*id);
762
763    // SAFETY: Borrow tracking ensures exclusive access
764    unsafe {
765        let component = &mut *component_ref.get();
766        component.release_subscriptions();
767    }
768
769    Ok(())
770}
771
772/// Returns a component from the global registry by ID.
773pub fn get_component(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
774    with_component_registry(|registry| registry.get(id))
775}
776
777/// Removes the component with `id` from the global registry.
778///
779/// Only the exact ID is removed, so unrelated components sharing the thread-local registry
780/// are untouched.
781pub fn deregister_component(id: &Ustr) {
782    with_component_registry(|registry| registry.remove(id));
783}
784
785#[cfg(test)]
786/// Clears the component registry (for test isolation).
787pub fn clear_component_registry() {
788    with_component_registry(|registry| {
789        registry.components.borrow_mut().clear();
790        registry.borrows.borrow_mut().clear();
791    });
792}
793
794#[cfg(test)]
795mod tests {
796    use std::{
797        any::Any,
798        sync::atomic::{AtomicBool, Ordering},
799    };
800
801    use rstest::rstest;
802
803    use super::*;
804
805    #[derive(Debug)]
806    struct TestComponent {
807        id: ComponentId,
808        state: ComponentState,
809        should_panic: &'static AtomicBool,
810        hooks_fail: bool,
811        releases: usize,
812    }
813
814    impl TestComponent {
815        fn new(name: &str, should_panic: &'static AtomicBool) -> Self {
816            Self {
817                id: ComponentId::new(name),
818                state: ComponentState::Ready,
819                should_panic,
820                hooks_fail: false,
821                releases: 0,
822            }
823        }
824
825        fn register_in_global_registry(self) -> Ustr {
826            let id = self.id.inner();
827            let component_ref: Rc<UnsafeCell<dyn Component>> = Rc::new(UnsafeCell::new(self));
828            with_component_registry(|registry| registry.insert(id, component_ref));
829            id
830        }
831    }
832
833    impl Actor for TestComponent {
834        fn id(&self) -> Ustr {
835            self.id.inner()
836        }
837
838        fn handle(&mut self, _msg: &dyn Any) {}
839
840        fn as_any(&self) -> &dyn Any {
841            self
842        }
843    }
844
845    impl Component for TestComponent {
846        fn component_id(&self) -> ComponentId {
847            self.id
848        }
849
850        fn state(&self) -> ComponentState {
851            self.state
852        }
853
854        fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()> {
855            self.state = self.state.transition(&trigger)?;
856            Ok(())
857        }
858
859        fn register(
860            &mut self,
861            _trader_id: TraderId,
862            _clock: Rc<RefCell<dyn Clock>>,
863            _cache: Rc<RefCell<Cache>>,
864        ) -> anyhow::Result<()> {
865            Ok(())
866        }
867
868        #[expect(clippy::panic_in_result_fn)] // Intentional panic for testing
869        fn on_start(&mut self) -> anyhow::Result<()> {
870            assert!(
871                !self.should_panic.load(Ordering::SeqCst),
872                "Intentional panic for testing"
873            );
874
875            if self.hooks_fail {
876                anyhow::bail!("on_start failed");
877            }
878
879            Ok(())
880        }
881
882        fn on_stop(&mut self) -> anyhow::Result<()> {
883            if self.hooks_fail {
884                anyhow::bail!("on_stop failed");
885            }
886
887            Ok(())
888        }
889
890        fn on_resume(&mut self) -> anyhow::Result<()> {
891            if self.hooks_fail {
892                anyhow::bail!("on_resume failed");
893            }
894
895            Ok(())
896        }
897
898        fn on_reset(&mut self) -> anyhow::Result<()> {
899            if self.hooks_fail {
900                anyhow::bail!("on_reset failed");
901            }
902
903            Ok(())
904        }
905
906        fn on_dispose(&mut self) -> anyhow::Result<()> {
907            if self.hooks_fail {
908                anyhow::bail!("on_dispose failed");
909            }
910
911            Ok(())
912        }
913
914        fn on_fault(&mut self) -> anyhow::Result<()> {
915            if self.hooks_fail {
916                anyhow::bail!("on_fault failed");
917            }
918
919            Ok(())
920        }
921
922        fn on_degrade(&mut self) -> anyhow::Result<()> {
923            if self.hooks_fail {
924                anyhow::bail!("on_degrade failed");
925            }
926
927            Ok(())
928        }
929
930        fn release_subscriptions(&mut self) {
931            self.releases += 1;
932        }
933    }
934
935    static NO_PANIC: AtomicBool = AtomicBool::new(false);
936    static DO_PANIC: AtomicBool = AtomicBool::new(true);
937
938    #[rstest]
939    fn test_component_borrow_tracking_prevents_double_borrow() {
940        clear_component_registry();
941
942        let id = Ustr::from("test-component-1");
943        let component = TestComponent::new("test-component-1", &NO_PANIC);
944        let component_id = component.id.inner();
945
946        let component_ref = Rc::new(UnsafeCell::new(component));
947        with_component_registry(|registry| registry.insert(component_id, component_ref));
948
949        // First borrow via start_component should succeed
950        start_component(&id).unwrap();
951        assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
952
953        // Component should now be borrowable again (guard released)
954        stop_component(&id).unwrap();
955        assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
956    }
957
958    #[rstest]
959    fn test_component_borrow_released_after_lifecycle_call() {
960        clear_component_registry();
961
962        let id = Ustr::from("test-component-2");
963        let component = TestComponent::new("test-component-2", &NO_PANIC);
964        let component_id = component.id.inner();
965
966        let component_ref = Rc::new(UnsafeCell::new(component));
967        with_component_registry(|registry| registry.insert(component_id, component_ref));
968
969        // Call start - borrow should be released after
970        let _ = start_component(&id);
971
972        // Verify not marked as borrowed
973        assert!(!with_component_registry(
974            |registry| registry.is_borrowed(&id)
975        ));
976    }
977
978    #[rstest]
979    fn test_component_borrow_released_on_panic() {
980        clear_component_registry();
981
982        let id = Ustr::from("test-component-panic");
983        let component = TestComponent::new("test-component-panic", &DO_PANIC);
984        let component_id = component.id.inner();
985
986        let component_ref = Rc::new(UnsafeCell::new(component));
987        with_component_registry(|registry| registry.insert(component_id, component_ref));
988
989        // Call start which will panic - catch the panic
990        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
991            let _ = start_component(&id);
992        }));
993        assert!(result.is_err(), "Expected panic from on_start");
994
995        // Borrow should still be released due to BorrowGuard drop
996        assert!(
997            !with_component_registry(|registry| registry.is_borrowed(&id)),
998            "Borrow was not released after panic"
999        );
1000    }
1001
1002    #[rstest]
1003    #[case(ComponentState::PreInitialized, ComponentTrigger::Start)]
1004    #[case(ComponentState::Ready, ComponentTrigger::Resume)]
1005    #[case(ComponentState::Running, ComponentTrigger::Start)]
1006    #[case(ComponentState::Stopped, ComponentTrigger::Stop)]
1007    #[case(ComponentState::Disposed, ComponentTrigger::Dispose)]
1008    #[case(ComponentState::Faulted, ComponentTrigger::Fault)]
1009    fn test_transition_rejects_invalid_trigger(
1010        #[case] state: ComponentState,
1011        #[case] trigger: ComponentTrigger,
1012    ) {
1013        let mut current = state;
1014
1015        let error = current.transition(&trigger).unwrap_err();
1016
1017        assert_eq!(
1018            error.to_string(),
1019            format!("Invalid state trigger {state} -> {trigger}")
1020        );
1021        assert_eq!(current, state, "Rejected trigger must not mutate the state");
1022    }
1023
1024    /// Covers every arm of the transition table, so deleting one is a failing test.
1025    #[rstest]
1026    #[case(
1027        ComponentState::PreInitialized,
1028        ComponentTrigger::Initialize,
1029        ComponentState::Ready
1030    )]
1031    #[case(
1032        ComponentState::Ready,
1033        ComponentTrigger::Reset,
1034        ComponentState::Resetting
1035    )]
1036    #[case(
1037        ComponentState::Ready,
1038        ComponentTrigger::Start,
1039        ComponentState::Starting
1040    )]
1041    #[case(
1042        ComponentState::Ready,
1043        ComponentTrigger::Dispose,
1044        ComponentState::Disposing
1045    )]
1046    #[case(
1047        ComponentState::Resetting,
1048        ComponentTrigger::ResetCompleted,
1049        ComponentState::Ready
1050    )]
1051    #[case(
1052        ComponentState::Starting,
1053        ComponentTrigger::StartCompleted,
1054        ComponentState::Running
1055    )]
1056    #[case(
1057        ComponentState::Starting,
1058        ComponentTrigger::Stop,
1059        ComponentState::Stopping
1060    )]
1061    #[case(
1062        ComponentState::Starting,
1063        ComponentTrigger::Fault,
1064        ComponentState::Faulting
1065    )]
1066    #[case(
1067        ComponentState::Running,
1068        ComponentTrigger::Stop,
1069        ComponentState::Stopping
1070    )]
1071    #[case(
1072        ComponentState::Running,
1073        ComponentTrigger::Degrade,
1074        ComponentState::Degrading
1075    )]
1076    #[case(
1077        ComponentState::Running,
1078        ComponentTrigger::Fault,
1079        ComponentState::Faulting
1080    )]
1081    #[case(
1082        ComponentState::Resuming,
1083        ComponentTrigger::Stop,
1084        ComponentState::Stopping
1085    )]
1086    #[case(
1087        ComponentState::Resuming,
1088        ComponentTrigger::ResumeCompleted,
1089        ComponentState::Running
1090    )]
1091    #[case(
1092        ComponentState::Resuming,
1093        ComponentTrigger::Fault,
1094        ComponentState::Faulting
1095    )]
1096    #[case(
1097        ComponentState::Stopping,
1098        ComponentTrigger::StopCompleted,
1099        ComponentState::Stopped
1100    )]
1101    #[case(
1102        ComponentState::Stopping,
1103        ComponentTrigger::Dispose,
1104        ComponentState::Disposing
1105    )]
1106    #[case(
1107        ComponentState::Stopping,
1108        ComponentTrigger::Fault,
1109        ComponentState::Faulting
1110    )]
1111    #[case(
1112        ComponentState::Stopped,
1113        ComponentTrigger::Reset,
1114        ComponentState::Resetting
1115    )]
1116    #[case(
1117        ComponentState::Stopped,
1118        ComponentTrigger::Resume,
1119        ComponentState::Resuming
1120    )]
1121    #[case(
1122        ComponentState::Stopped,
1123        ComponentTrigger::Dispose,
1124        ComponentState::Disposing
1125    )]
1126    #[case(
1127        ComponentState::Stopped,
1128        ComponentTrigger::Fault,
1129        ComponentState::Faulting
1130    )]
1131    #[case(
1132        ComponentState::Degrading,
1133        ComponentTrigger::DegradeCompleted,
1134        ComponentState::Degraded
1135    )]
1136    #[case(
1137        ComponentState::Degraded,
1138        ComponentTrigger::Resume,
1139        ComponentState::Resuming
1140    )]
1141    #[case(
1142        ComponentState::Degraded,
1143        ComponentTrigger::Stop,
1144        ComponentState::Stopping
1145    )]
1146    #[case(
1147        ComponentState::Degraded,
1148        ComponentTrigger::Fault,
1149        ComponentState::Faulting
1150    )]
1151    #[case(
1152        ComponentState::Disposing,
1153        ComponentTrigger::DisposeCompleted,
1154        ComponentState::Disposed
1155    )]
1156    #[case(
1157        ComponentState::Disposing,
1158        ComponentTrigger::Fault,
1159        ComponentState::Faulting
1160    )]
1161    #[case(
1162        ComponentState::Faulting,
1163        ComponentTrigger::Dispose,
1164        ComponentState::Disposing
1165    )]
1166    #[case(
1167        ComponentState::Faulting,
1168        ComponentTrigger::FaultCompleted,
1169        ComponentState::Faulted
1170    )]
1171    fn test_transition_accepts_valid_trigger(
1172        #[case] state: ComponentState,
1173        #[case] trigger: ComponentTrigger,
1174        #[case] expected: ComponentState,
1175    ) {
1176        let mut current = state;
1177
1178        assert_eq!(current.transition(&trigger).unwrap(), expected);
1179    }
1180
1181    #[rstest]
1182    fn test_state_predicates_match_the_current_state() {
1183        let mut component = TestComponent::new("predicates", &NO_PANIC);
1184
1185        assert!(component.is_ready());
1186        assert!(component.not_running());
1187
1188        component.start().unwrap();
1189        assert!(component.is_running());
1190        assert!(!component.not_running());
1191        assert!(!component.is_ready());
1192
1193        component.stop().unwrap();
1194        assert!(component.is_stopped());
1195        assert!(!component.is_running());
1196
1197        component.resume().unwrap();
1198        component.degrade().unwrap();
1199        assert!(component.is_degraded());
1200        assert!(!component.is_stopped());
1201
1202        component.fault().unwrap();
1203        assert!(component.is_faulted());
1204        assert!(!component.is_degraded());
1205        assert!(!component.is_disposed());
1206    }
1207
1208    #[rstest]
1209    fn test_is_disposed_only_after_disposal() {
1210        let mut component = TestComponent::new("disposable", &NO_PANIC);
1211
1212        assert!(!component.is_disposed());
1213
1214        component.dispose().unwrap();
1215
1216        assert!(component.is_disposed());
1217        assert!(!component.is_ready());
1218        assert!(!component.is_faulted());
1219    }
1220
1221    #[rstest]
1222    fn test_degrade_halts_transition_when_on_degrade_fails() {
1223        let mut component = TestComponent::new("failing-degrade", &NO_PANIC);
1224        component.start().unwrap();
1225        component.hooks_fail = true;
1226
1227        let error = component.degrade().unwrap_err();
1228
1229        assert_eq!(error.to_string(), "on_degrade failed");
1230        assert_eq!(component.state(), ComponentState::Degrading);
1231    }
1232
1233    #[rstest]
1234    fn test_initialize_advances_from_pre_initialized() {
1235        let mut component = TestComponent::new("initializing", &NO_PANIC);
1236        component.state = ComponentState::PreInitialized;
1237
1238        component.initialize().unwrap();
1239
1240        assert_eq!(component.state(), ComponentState::Ready);
1241    }
1242
1243    #[rstest]
1244    fn test_start_halts_transition_when_on_start_fails() {
1245        let mut component = TestComponent::new("failing-start", &NO_PANIC);
1246        component.hooks_fail = true;
1247
1248        let error = component.start().unwrap_err();
1249
1250        assert_eq!(error.to_string(), "on_start failed");
1251        assert_eq!(component.state(), ComponentState::Starting);
1252    }
1253
1254    #[rstest]
1255    fn test_stop_halts_transition_when_on_stop_fails() {
1256        let mut component = TestComponent::new("failing-stop", &NO_PANIC);
1257        component.start().unwrap();
1258        component.hooks_fail = true;
1259
1260        let error = component.stop().unwrap_err();
1261
1262        assert_eq!(error.to_string(), "on_stop failed");
1263        assert_eq!(component.state(), ComponentState::Stopping);
1264    }
1265
1266    #[rstest]
1267    fn test_resume_halts_transition_when_on_resume_fails() {
1268        let mut component = TestComponent::new("failing-resume", &NO_PANIC);
1269        component.start().unwrap();
1270        component.stop().unwrap();
1271        component.hooks_fail = true;
1272
1273        let error = component.resume().unwrap_err();
1274
1275        assert_eq!(error.to_string(), "on_resume failed");
1276        assert_eq!(component.state(), ComponentState::Resuming);
1277    }
1278
1279    #[rstest]
1280    fn test_reset_retains_subscriptions_when_on_reset_fails() {
1281        let mut component = TestComponent::new("failing-reset", &NO_PANIC);
1282        component.hooks_fail = true;
1283
1284        let error = component.reset().unwrap_err();
1285
1286        assert_eq!(error.to_string(), "on_reset failed");
1287        assert_eq!(component.state(), ComponentState::Resetting);
1288        assert_eq!(component.releases, 0);
1289    }
1290
1291    #[rstest]
1292    fn test_reset_releases_subscriptions_on_success() {
1293        let mut component = TestComponent::new("resetting", &NO_PANIC);
1294
1295        component.reset().unwrap();
1296
1297        assert_eq!(component.state(), ComponentState::Ready);
1298        assert_eq!(component.releases, 1);
1299    }
1300
1301    #[rstest]
1302    fn test_dispose_faults_and_retains_subscriptions_when_on_dispose_fails() {
1303        let mut component = TestComponent::new("failing-dispose", &NO_PANIC);
1304        component.hooks_fail = true;
1305
1306        let error = component.dispose().unwrap_err();
1307
1308        assert_eq!(error.to_string(), "on_dispose failed");
1309        assert_eq!(component.state(), ComponentState::Faulted);
1310        assert_eq!(component.releases, 0);
1311    }
1312
1313    #[rstest]
1314    fn test_dispose_releases_subscriptions_on_success() {
1315        let mut component = TestComponent::new("disposing", &NO_PANIC);
1316
1317        component.dispose().unwrap();
1318
1319        assert_eq!(component.state(), ComponentState::Disposed);
1320        assert_eq!(component.releases, 1);
1321    }
1322
1323    #[rstest]
1324    fn test_fault_releases_subscriptions_even_when_on_fault_fails() {
1325        let mut component = TestComponent::new("failing-fault", &NO_PANIC);
1326        component.start().unwrap();
1327        component.hooks_fail = true;
1328
1329        let error = component.fault().unwrap_err();
1330
1331        assert_eq!(error.to_string(), "on_fault failed");
1332        assert_eq!(component.state(), ComponentState::Faulting);
1333        assert_eq!(component.releases, 1);
1334    }
1335
1336    #[rstest]
1337    fn test_fault_releases_subscriptions_on_success() {
1338        let mut component = TestComponent::new("faulting", &NO_PANIC);
1339        component.start().unwrap();
1340
1341        component.fault().unwrap();
1342
1343        assert_eq!(component.state(), ComponentState::Faulted);
1344        assert_eq!(component.releases, 1);
1345    }
1346
1347    #[rstest]
1348    fn test_registry_entry_points_reject_unknown_component() {
1349        clear_component_registry();
1350
1351        let id = Ustr::from("absent-component");
1352
1353        for (name, entry_point) in registry_entry_points() {
1354            let error = entry_point(&id).unwrap_err();
1355            assert_eq!(
1356                error.to_string(),
1357                "Component 'absent-component' not found in global registry",
1358                "unexpected error from {name}"
1359            );
1360        }
1361
1362        let error = component_state(&id).unwrap_err();
1363        assert_eq!(
1364            error.to_string(),
1365            "Component 'absent-component' not found in global registry"
1366        );
1367    }
1368
1369    #[rstest]
1370    fn test_registry_entry_points_reject_borrowed_component() {
1371        clear_component_registry();
1372
1373        let id = TestComponent::new("borrowed-component", &NO_PANIC).register_in_global_registry();
1374        assert!(with_component_registry(|registry| registry.try_borrow(id)));
1375
1376        for (name, entry_point) in registry_entry_points() {
1377            let error = entry_point(&id).unwrap_err();
1378            assert!(
1379                error
1380                    .to_string()
1381                    .starts_with("Component 'borrowed-component' is already mutably borrowed."),
1382                "unexpected error from {name}: {error}"
1383            );
1384        }
1385
1386        let error = component_state(&id).unwrap_err();
1387        assert!(
1388            error
1389                .to_string()
1390                .starts_with("Component 'borrowed-component' is already mutably borrowed.")
1391        );
1392
1393        // The rejected calls must leave the original borrow in place
1394        assert!(with_component_registry(|registry| registry.is_borrowed(&id)));
1395    }
1396
1397    type RegistryEntryPoint = (&'static str, fn(&Ustr) -> anyhow::Result<()>);
1398
1399    /// Lifecycle entry points that resolve a component from the global registry.
1400    fn registry_entry_points() -> [RegistryEntryPoint; 5] {
1401        [
1402            ("start", start_component),
1403            ("stop", stop_component),
1404            ("reset", reset_component),
1405            ("dispose", dispose_component),
1406            ("release", release_component_subscriptions),
1407        ]
1408    }
1409
1410    #[rstest]
1411    fn test_registry_entry_points_drive_the_full_lifecycle() {
1412        clear_component_registry();
1413
1414        let id = TestComponent::new("lifecycle-component", &NO_PANIC).register_in_global_registry();
1415
1416        assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1417
1418        start_component(&id).unwrap();
1419        assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
1420
1421        stop_component(&id).unwrap();
1422        assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
1423
1424        reset_component(&id).unwrap();
1425        assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1426
1427        dispose_component(&id).unwrap();
1428        assert_eq!(component_state(&id).unwrap(), ComponentState::Disposed);
1429
1430        assert!(!with_component_registry(
1431            |registry| registry.is_borrowed(&id)
1432        ));
1433    }
1434
1435    #[rstest]
1436    fn test_release_component_subscriptions_invokes_the_component_hook() {
1437        clear_component_registry();
1438
1439        let component = TestComponent::new("releasing-component", &NO_PANIC);
1440        let id = component.id.inner();
1441        let component_ref = Rc::new(UnsafeCell::new(component));
1442        let observed = component_ref.clone();
1443        with_component_registry(|registry| {
1444            registry.insert(id, component_ref as Rc<UnsafeCell<dyn Component>>);
1445        });
1446
1447        release_component_subscriptions(&id).unwrap();
1448        release_component_subscriptions(&id).unwrap();
1449
1450        // SAFETY: no lifecycle call is in flight, so no other reference exists
1451        assert_eq!(unsafe { (*observed.get()).releases }, 2);
1452    }
1453
1454    #[rstest]
1455    fn test_deregister_component_removes_only_the_named_component() {
1456        clear_component_registry();
1457
1458        let kept = TestComponent::new("kept-component", &NO_PANIC).register_in_global_registry();
1459        let removed =
1460            TestComponent::new("removed-component", &NO_PANIC).register_in_global_registry();
1461
1462        deregister_component(&removed);
1463
1464        assert!(get_component(&removed).is_none());
1465        assert!(get_component(&kept).is_some());
1466        assert_eq!(component_state(&kept).unwrap(), ComponentState::Ready);
1467    }
1468
1469    #[rstest]
1470    fn test_component_registry_debug_reports_components_and_borrows() {
1471        clear_component_registry();
1472
1473        let id = TestComponent::new("debug-component", &NO_PANIC).register_in_global_registry();
1474        assert!(with_component_registry(|registry| registry.try_borrow(id)));
1475
1476        let debug = with_component_registry(|registry| format!("{registry:?}"));
1477
1478        assert!(debug.contains("debug-component"), "{debug}");
1479        assert!(debug.contains("active_borrows: 1"), "{debug}");
1480    }
1481
1482    #[rstest]
1483    fn test_registry_remove_returns_the_registered_component() {
1484        clear_component_registry();
1485
1486        let id = TestComponent::new("removable", &NO_PANIC).register_in_global_registry();
1487
1488        assert!(with_component_registry(|registry| registry.remove(&id)).is_some());
1489        assert!(with_component_registry(|registry| registry.remove(&id)).is_none());
1490    }
1491}