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