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:
558/// - The component is not found.
559/// - The component is already borrowed.
560/// - `start()` fails.
561pub fn start_component(id: &Ustr) -> anyhow::Result<()> {
562    let component_ref = with_component_registry(|registry| {
563        let component_ref = registry
564            .get(id)
565            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
566
567        if !registry.try_borrow(*id) {
568            anyhow::bail!(
569                "Component '{id}' is already mutably borrowed. \
570                 This would create aliasing mutable references (undefined behavior)."
571            );
572        }
573
574        Ok::<_, anyhow::Error>(component_ref)
575    })?;
576
577    let _guard = BorrowGuard::new(*id);
578
579    // SAFETY: Borrow tracking ensures exclusive access
580    unsafe {
581        let component = &mut *component_ref.get();
582        component.start()
583    }
584}
585
586/// Returns the state of a component in the global registry.
587///
588/// # Errors
589///
590/// Returns an error if:
591/// - The component is not found.
592/// - The component is already borrowed.
593pub fn component_state(id: &Ustr) -> anyhow::Result<ComponentState> {
594    let component_ref = with_component_registry(|registry| {
595        let component_ref = registry
596            .get(id)
597            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
598
599        if !registry.try_borrow(*id) {
600            anyhow::bail!(
601                "Component '{id}' is already mutably borrowed. \
602                 This would create aliasing mutable references (undefined behavior)."
603            );
604        }
605
606        Ok::<_, anyhow::Error>(component_ref)
607    })?;
608
609    let _guard = BorrowGuard::new(*id);
610
611    // SAFETY: Borrow tracking ensures there is no concurrent mutable lifecycle access.
612    unsafe {
613        let component = &*component_ref.get();
614        Ok(component.state())
615    }
616}
617
618/// Safely calls `stop()` on a component in the global registry.
619///
620/// # Errors
621///
622/// Returns an error if:
623/// - The component is not found.
624/// - The component is already borrowed.
625/// - `stop()` fails.
626pub fn stop_component(id: &Ustr) -> anyhow::Result<()> {
627    let component_ref = with_component_registry(|registry| {
628        let component_ref = registry
629            .get(id)
630            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
631
632        if !registry.try_borrow(*id) {
633            anyhow::bail!(
634                "Component '{id}' is already mutably borrowed. \
635                 This would create aliasing mutable references (undefined behavior)."
636            );
637        }
638
639        Ok::<_, anyhow::Error>(component_ref)
640    })?;
641
642    let _guard = BorrowGuard::new(*id);
643
644    // SAFETY: Borrow tracking ensures exclusive access
645    unsafe {
646        let component = &mut *component_ref.get();
647        component.stop()
648    }
649}
650
651/// Safely calls `reset()` on a component in the global registry.
652///
653/// # Errors
654///
655/// Returns an error if:
656/// - The component is not found.
657/// - The component is already borrowed.
658/// - `reset()` fails.
659pub fn reset_component(id: &Ustr) -> anyhow::Result<()> {
660    let component_ref = with_component_registry(|registry| {
661        let component_ref = registry
662            .get(id)
663            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
664
665        if !registry.try_borrow(*id) {
666            anyhow::bail!(
667                "Component '{id}' is already mutably borrowed. \
668                 This would create aliasing mutable references (undefined behavior)."
669            );
670        }
671
672        Ok::<_, anyhow::Error>(component_ref)
673    })?;
674
675    let _guard = BorrowGuard::new(*id);
676
677    // SAFETY: Borrow tracking ensures exclusive access
678    unsafe {
679        let component = &mut *component_ref.get();
680        component.reset()
681    }
682}
683
684/// Safely calls `dispose()` on a component in the global registry.
685///
686/// # Errors
687///
688/// Returns an error if:
689/// - The component is not found.
690/// - The component is already borrowed.
691/// - `dispose()` fails.
692pub fn dispose_component(id: &Ustr) -> anyhow::Result<()> {
693    let component_ref = with_component_registry(|registry| {
694        let component_ref = registry
695            .get(id)
696            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
697
698        if !registry.try_borrow(*id) {
699            anyhow::bail!(
700                "Component '{id}' is already mutably borrowed. \
701                 This would create aliasing mutable references (undefined behavior)."
702            );
703        }
704
705        Ok::<_, anyhow::Error>(component_ref)
706    })?;
707
708    let _guard = BorrowGuard::new(*id);
709
710    // SAFETY: Borrow tracking ensures exclusive access
711    unsafe {
712        let component = &mut *component_ref.get();
713        component.dispose()
714    }
715}
716
717/// Releases subscriptions for a component in the global registry.
718///
719/// This is used when retiring a component whose earlier `on_dispose` failed after the framework
720/// left its registration intact.
721///
722/// # Errors
723///
724/// Returns an error if:
725/// - The component is not found.
726/// - The component is already borrowed.
727pub fn release_component_subscriptions(id: &Ustr) -> anyhow::Result<()> {
728    let component_ref = with_component_registry(|registry| {
729        let component_ref = registry
730            .get(id)
731            .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
732
733        if !registry.try_borrow(*id) {
734            anyhow::bail!(
735                "Component '{id}' is already mutably borrowed. \
736                 This would create aliasing mutable references (undefined behavior)."
737            );
738        }
739
740        Ok::<_, anyhow::Error>(component_ref)
741    })?;
742
743    let _guard = BorrowGuard::new(*id);
744
745    // SAFETY: Borrow tracking ensures exclusive access
746    unsafe {
747        let component = &mut *component_ref.get();
748        component.release_subscriptions();
749    }
750
751    Ok(())
752}
753
754/// Returns a component from the global registry by ID.
755pub fn get_component(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
756    with_component_registry(|registry| registry.get(id))
757}
758
759/// Removes the component with `id` from the global registry.
760///
761/// Only the exact ID is removed, so unrelated components sharing the thread-local registry
762/// are untouched.
763pub fn deregister_component(id: &Ustr) {
764    with_component_registry(|registry| registry.remove(id));
765}
766
767#[cfg(test)]
768/// Clears the component registry (for test isolation).
769pub fn clear_component_registry() {
770    with_component_registry(|registry| {
771        registry.components.borrow_mut().clear();
772        registry.borrows.borrow_mut().clear();
773    });
774}
775
776#[cfg(test)]
777mod tests {
778    use std::{
779        any::Any,
780        sync::atomic::{AtomicBool, Ordering},
781    };
782
783    use nautilus_core::UUID4;
784    use rstest::rstest;
785
786    use super::*;
787    use crate::logging::{
788        arm_shutdown_on_error, disarm_shutdown_on_error, init_logging,
789        take_shutdown_on_error_trigger,
790    };
791
792    #[derive(Debug)]
793    struct TestComponent {
794        id: ComponentId,
795        state: ComponentState,
796        should_panic: &'static AtomicBool,
797        hooks_fail: bool,
798        releases: usize,
799    }
800
801    impl TestComponent {
802        fn new(name: &str, should_panic: &'static AtomicBool) -> Self {
803            Self {
804                id: ComponentId::new(name),
805                state: ComponentState::Ready,
806                should_panic,
807                hooks_fail: false,
808                releases: 0,
809            }
810        }
811
812        fn register_in_global_registry(self) -> Ustr {
813            let id = self.id.inner();
814            let component_ref: Rc<UnsafeCell<dyn Component>> = Rc::new(UnsafeCell::new(self));
815            with_component_registry(|registry| registry.insert(id, component_ref));
816            id
817        }
818    }
819
820    impl Actor for TestComponent {
821        fn id(&self) -> Ustr {
822            self.id.inner()
823        }
824
825        fn handle(&mut self, _msg: &dyn Any) {}
826
827        fn as_any(&self) -> &dyn Any {
828            self
829        }
830    }
831
832    impl Component for TestComponent {
833        fn component_id(&self) -> ComponentId {
834            self.id
835        }
836
837        fn state(&self) -> ComponentState {
838            self.state
839        }
840
841        fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()> {
842            self.state = self.state.transition(&trigger)?;
843            Ok(())
844        }
845
846        fn register(
847            &mut self,
848            _trader_id: TraderId,
849            _clock: Rc<RefCell<dyn Clock>>,
850            _cache: Rc<RefCell<Cache>>,
851        ) -> anyhow::Result<()> {
852            Ok(())
853        }
854
855        #[expect(clippy::panic_in_result_fn)] // Intentional panic for testing
856        fn on_start(&mut self) -> anyhow::Result<()> {
857            assert!(
858                !self.should_panic.load(Ordering::SeqCst),
859                "Intentional panic for testing"
860            );
861
862            if self.hooks_fail {
863                anyhow::bail!("on_start failed");
864            }
865
866            Ok(())
867        }
868
869        fn on_stop(&mut self) -> anyhow::Result<()> {
870            if self.hooks_fail {
871                anyhow::bail!("on_stop failed");
872            }
873
874            Ok(())
875        }
876
877        fn on_resume(&mut self) -> anyhow::Result<()> {
878            if self.hooks_fail {
879                anyhow::bail!("on_resume failed");
880            }
881
882            Ok(())
883        }
884
885        fn on_reset(&mut self) -> anyhow::Result<()> {
886            if self.hooks_fail {
887                anyhow::bail!("on_reset failed");
888            }
889
890            Ok(())
891        }
892
893        fn on_dispose(&mut self) -> anyhow::Result<()> {
894            if self.hooks_fail {
895                anyhow::bail!("on_dispose failed");
896            }
897
898            Ok(())
899        }
900
901        fn on_fault(&mut self) -> anyhow::Result<()> {
902            if self.hooks_fail {
903                anyhow::bail!("on_fault failed");
904            }
905
906            Ok(())
907        }
908
909        fn on_degrade(&mut self) -> anyhow::Result<()> {
910            if self.hooks_fail {
911                anyhow::bail!("on_degrade failed");
912            }
913
914            Ok(())
915        }
916
917        fn release_subscriptions(&mut self) {
918            self.releases += 1;
919        }
920    }
921
922    static NO_PANIC: AtomicBool = AtomicBool::new(false);
923    static DO_PANIC: AtomicBool = AtomicBool::new(true);
924
925    #[rstest]
926    fn test_component_borrow_tracking_prevents_double_borrow() {
927        clear_component_registry();
928
929        let id = Ustr::from("test-component-1");
930        let component = TestComponent::new("test-component-1", &NO_PANIC);
931        let component_id = component.id.inner();
932
933        let component_ref = Rc::new(UnsafeCell::new(component));
934        with_component_registry(|registry| registry.insert(component_id, component_ref));
935
936        // First borrow via start_component should succeed
937        start_component(&id).unwrap();
938        assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
939
940        // Component should now be borrowable again (guard released)
941        stop_component(&id).unwrap();
942        assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
943    }
944
945    #[rstest]
946    fn test_component_borrow_released_after_lifecycle_call() {
947        clear_component_registry();
948
949        let id = Ustr::from("test-component-2");
950        let component = TestComponent::new("test-component-2", &NO_PANIC);
951        let component_id = component.id.inner();
952
953        let component_ref = Rc::new(UnsafeCell::new(component));
954        with_component_registry(|registry| registry.insert(component_id, component_ref));
955
956        // Call start - borrow should be released after
957        let _ = start_component(&id);
958
959        // Verify not marked as borrowed
960        assert!(!with_component_registry(
961            |registry| registry.is_borrowed(&id)
962        ));
963    }
964
965    #[rstest]
966    fn test_component_borrow_released_on_panic() {
967        clear_component_registry();
968
969        let id = Ustr::from("test-component-panic");
970        let component = TestComponent::new("test-component-panic", &DO_PANIC);
971        let component_id = component.id.inner();
972
973        let component_ref = Rc::new(UnsafeCell::new(component));
974        with_component_registry(|registry| registry.insert(component_id, component_ref));
975
976        // Call start which will panic - catch the panic
977        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
978            let _ = start_component(&id);
979        }));
980        assert!(result.is_err(), "Expected panic from on_start");
981
982        // Borrow should still be released due to BorrowGuard drop
983        assert!(
984            !with_component_registry(|registry| registry.is_borrowed(&id)),
985            "Borrow was not released after panic"
986        );
987    }
988
989    #[rstest]
990    #[case(ComponentState::PreInitialized, ComponentTrigger::Start)]
991    #[case(ComponentState::Ready, ComponentTrigger::Resume)]
992    #[case(ComponentState::Running, ComponentTrigger::Start)]
993    #[case(ComponentState::Stopped, ComponentTrigger::Stop)]
994    #[case(ComponentState::Disposed, ComponentTrigger::Dispose)]
995    #[case(ComponentState::Faulted, ComponentTrigger::Fault)]
996    fn test_transition_rejects_invalid_trigger(
997        #[case] state: ComponentState,
998        #[case] trigger: ComponentTrigger,
999    ) {
1000        let mut current = state;
1001
1002        let error = current.transition(&trigger).unwrap_err();
1003
1004        assert_eq!(
1005            error.to_string(),
1006            format!("Invalid state trigger {state} -> {trigger}")
1007        );
1008        assert_eq!(current, state, "Rejected trigger must not mutate the state");
1009    }
1010
1011    /// Covers every arm of the transition table, so deleting one is a failing test.
1012    #[rstest]
1013    #[case(
1014        ComponentState::PreInitialized,
1015        ComponentTrigger::Initialize,
1016        ComponentState::Ready
1017    )]
1018    #[case(
1019        ComponentState::Ready,
1020        ComponentTrigger::Reset,
1021        ComponentState::Resetting
1022    )]
1023    #[case(
1024        ComponentState::Ready,
1025        ComponentTrigger::Start,
1026        ComponentState::Starting
1027    )]
1028    #[case(
1029        ComponentState::Ready,
1030        ComponentTrigger::Dispose,
1031        ComponentState::Disposing
1032    )]
1033    #[case(
1034        ComponentState::Resetting,
1035        ComponentTrigger::ResetCompleted,
1036        ComponentState::Ready
1037    )]
1038    #[case(
1039        ComponentState::Starting,
1040        ComponentTrigger::StartCompleted,
1041        ComponentState::Running
1042    )]
1043    #[case(
1044        ComponentState::Starting,
1045        ComponentTrigger::Stop,
1046        ComponentState::Stopping
1047    )]
1048    #[case(
1049        ComponentState::Starting,
1050        ComponentTrigger::Fault,
1051        ComponentState::Faulting
1052    )]
1053    #[case(
1054        ComponentState::Running,
1055        ComponentTrigger::Stop,
1056        ComponentState::Stopping
1057    )]
1058    #[case(
1059        ComponentState::Running,
1060        ComponentTrigger::Degrade,
1061        ComponentState::Degrading
1062    )]
1063    #[case(
1064        ComponentState::Running,
1065        ComponentTrigger::Fault,
1066        ComponentState::Faulting
1067    )]
1068    #[case(
1069        ComponentState::Resuming,
1070        ComponentTrigger::Stop,
1071        ComponentState::Stopping
1072    )]
1073    #[case(
1074        ComponentState::Resuming,
1075        ComponentTrigger::ResumeCompleted,
1076        ComponentState::Running
1077    )]
1078    #[case(
1079        ComponentState::Resuming,
1080        ComponentTrigger::Fault,
1081        ComponentState::Faulting
1082    )]
1083    #[case(
1084        ComponentState::Stopping,
1085        ComponentTrigger::StopCompleted,
1086        ComponentState::Stopped
1087    )]
1088    #[case(
1089        ComponentState::Stopping,
1090        ComponentTrigger::Dispose,
1091        ComponentState::Disposing
1092    )]
1093    #[case(
1094        ComponentState::Stopping,
1095        ComponentTrigger::Fault,
1096        ComponentState::Faulting
1097    )]
1098    #[case(
1099        ComponentState::Stopped,
1100        ComponentTrigger::Reset,
1101        ComponentState::Resetting
1102    )]
1103    #[case(
1104        ComponentState::Stopped,
1105        ComponentTrigger::Resume,
1106        ComponentState::Resuming
1107    )]
1108    #[case(
1109        ComponentState::Stopped,
1110        ComponentTrigger::Dispose,
1111        ComponentState::Disposing
1112    )]
1113    #[case(
1114        ComponentState::Stopped,
1115        ComponentTrigger::Fault,
1116        ComponentState::Faulting
1117    )]
1118    #[case(
1119        ComponentState::Degrading,
1120        ComponentTrigger::DegradeCompleted,
1121        ComponentState::Degraded
1122    )]
1123    #[case(
1124        ComponentState::Degraded,
1125        ComponentTrigger::Resume,
1126        ComponentState::Resuming
1127    )]
1128    #[case(
1129        ComponentState::Degraded,
1130        ComponentTrigger::Stop,
1131        ComponentState::Stopping
1132    )]
1133    #[case(
1134        ComponentState::Degraded,
1135        ComponentTrigger::Fault,
1136        ComponentState::Faulting
1137    )]
1138    #[case(
1139        ComponentState::Disposing,
1140        ComponentTrigger::DisposeCompleted,
1141        ComponentState::Disposed
1142    )]
1143    #[case(
1144        ComponentState::Disposing,
1145        ComponentTrigger::Fault,
1146        ComponentState::Faulting
1147    )]
1148    #[case(
1149        ComponentState::Faulting,
1150        ComponentTrigger::Dispose,
1151        ComponentState::Disposing
1152    )]
1153    #[case(
1154        ComponentState::Faulting,
1155        ComponentTrigger::FaultCompleted,
1156        ComponentState::Faulted
1157    )]
1158    fn test_transition_accepts_valid_trigger(
1159        #[case] state: ComponentState,
1160        #[case] trigger: ComponentTrigger,
1161        #[case] expected: ComponentState,
1162    ) {
1163        let mut current = state;
1164
1165        assert_eq!(current.transition(&trigger).unwrap(), expected);
1166    }
1167
1168    #[rstest]
1169    fn test_state_predicates_match_the_current_state() {
1170        let mut component = TestComponent::new("predicates", &NO_PANIC);
1171
1172        assert!(component.is_ready());
1173        assert!(component.not_running());
1174
1175        component.start().unwrap();
1176        assert!(component.is_running());
1177        assert!(!component.not_running());
1178        assert!(!component.is_ready());
1179
1180        component.stop().unwrap();
1181        assert!(component.is_stopped());
1182        assert!(!component.is_running());
1183
1184        component.resume().unwrap();
1185        component.degrade().unwrap();
1186        assert!(component.is_degraded());
1187        assert!(!component.is_stopped());
1188
1189        component.fault().unwrap();
1190        assert!(component.is_faulted());
1191        assert!(!component.is_degraded());
1192        assert!(!component.is_disposed());
1193    }
1194
1195    #[rstest]
1196    fn test_is_disposed_only_after_disposal() {
1197        let mut component = TestComponent::new("disposable", &NO_PANIC);
1198
1199        assert!(!component.is_disposed());
1200
1201        component.dispose().unwrap();
1202
1203        assert!(component.is_disposed());
1204        assert!(!component.is_ready());
1205        assert!(!component.is_faulted());
1206    }
1207
1208    #[rstest]
1209    fn test_degrade_halts_transition_when_on_degrade_fails() {
1210        let mut component = TestComponent::new("failing-degrade", &NO_PANIC);
1211        component.start().unwrap();
1212        component.hooks_fail = true;
1213
1214        let error = component.degrade().unwrap_err();
1215
1216        assert_eq!(error.to_string(), "on_degrade failed");
1217        assert_eq!(component.state(), ComponentState::Degrading);
1218    }
1219
1220    #[rstest]
1221    fn test_initialize_advances_from_pre_initialized() {
1222        let mut component = TestComponent::new("initializing", &NO_PANIC);
1223        component.state = ComponentState::PreInitialized;
1224
1225        component.initialize().unwrap();
1226
1227        assert_eq!(component.state(), ComponentState::Ready);
1228    }
1229
1230    #[rstest]
1231    fn test_start_halts_transition_when_on_start_fails() {
1232        let mut component = TestComponent::new("failing-start", &NO_PANIC);
1233        component.hooks_fail = true;
1234
1235        let error = component.start().unwrap_err();
1236
1237        assert_eq!(error.to_string(), "on_start failed");
1238        assert_eq!(component.state(), ComponentState::Starting);
1239    }
1240
1241    #[rstest]
1242    fn test_stop_halts_transition_when_on_stop_fails() {
1243        let mut component = TestComponent::new("failing-stop", &NO_PANIC);
1244        component.start().unwrap();
1245        component.hooks_fail = true;
1246
1247        let error = component.stop().unwrap_err();
1248
1249        assert_eq!(error.to_string(), "on_stop failed");
1250        assert_eq!(component.state(), ComponentState::Stopping);
1251    }
1252
1253    #[rstest]
1254    fn test_resume_halts_transition_when_on_resume_fails() {
1255        let mut component = TestComponent::new("failing-resume", &NO_PANIC);
1256        component.start().unwrap();
1257        component.stop().unwrap();
1258        component.hooks_fail = true;
1259
1260        let error = component.resume().unwrap_err();
1261
1262        assert_eq!(error.to_string(), "on_resume failed");
1263        assert_eq!(component.state(), ComponentState::Resuming);
1264    }
1265
1266    #[rstest]
1267    fn test_reset_retains_subscriptions_when_on_reset_fails() {
1268        let mut component = TestComponent::new("failing-reset", &NO_PANIC);
1269        component.hooks_fail = true;
1270
1271        let error = component.reset().unwrap_err();
1272
1273        assert_eq!(error.to_string(), "on_reset failed");
1274        assert_eq!(component.state(), ComponentState::Resetting);
1275        assert_eq!(component.releases, 0);
1276    }
1277
1278    #[rstest]
1279    fn test_reset_releases_subscriptions_on_success() {
1280        let mut component = TestComponent::new("resetting", &NO_PANIC);
1281
1282        component.reset().unwrap();
1283
1284        assert_eq!(component.state(), ComponentState::Ready);
1285        assert_eq!(component.releases, 1);
1286    }
1287
1288    #[rstest]
1289    fn test_dispose_faults_and_retains_subscriptions_when_on_dispose_fails() {
1290        let mut component = TestComponent::new("failing-dispose", &NO_PANIC);
1291        component.hooks_fail = true;
1292
1293        let error = component.dispose().unwrap_err();
1294
1295        assert_eq!(error.to_string(), "on_dispose failed");
1296        assert_eq!(component.state(), ComponentState::Faulted);
1297        assert_eq!(component.releases, 0);
1298    }
1299
1300    #[rstest]
1301    fn test_dispose_releases_subscriptions_on_success() {
1302        let mut component = TestComponent::new("disposing", &NO_PANIC);
1303
1304        component.dispose().unwrap();
1305
1306        assert_eq!(component.state(), ComponentState::Disposed);
1307        assert_eq!(component.releases, 1);
1308    }
1309
1310    #[rstest]
1311    fn test_fault_releases_subscriptions_even_when_on_fault_fails() {
1312        let mut component = TestComponent::new("failing-fault", &NO_PANIC);
1313        component.start().unwrap();
1314        component.hooks_fail = true;
1315
1316        let error = component.fault().unwrap_err();
1317
1318        assert_eq!(error.to_string(), "on_fault failed");
1319        assert_eq!(component.state(), ComponentState::Faulting);
1320        assert_eq!(component.releases, 1);
1321    }
1322
1323    #[rstest]
1324    fn test_fault_releases_subscriptions_on_success() {
1325        let mut component = TestComponent::new("faulting", &NO_PANIC);
1326        component.start().unwrap();
1327
1328        component.fault().unwrap();
1329
1330        assert_eq!(component.state(), ComponentState::Faulted);
1331        assert_eq!(component.releases, 1);
1332    }
1333
1334    #[rstest]
1335    fn test_registry_entry_points_reject_unknown_component() {
1336        clear_component_registry();
1337
1338        let id = Ustr::from("absent-component");
1339
1340        for (name, entry_point) in registry_entry_points() {
1341            let error = entry_point(&id).unwrap_err();
1342            assert_eq!(
1343                error.to_string(),
1344                "Component 'absent-component' not found in global registry",
1345                "unexpected error from {name}"
1346            );
1347        }
1348
1349        let error = component_state(&id).unwrap_err();
1350        assert_eq!(
1351            error.to_string(),
1352            "Component 'absent-component' not found in global registry"
1353        );
1354    }
1355
1356    #[rstest]
1357    fn test_registry_entry_points_reject_borrowed_component() {
1358        clear_component_registry();
1359
1360        let id = TestComponent::new("borrowed-component", &NO_PANIC).register_in_global_registry();
1361        assert!(with_component_registry(|registry| registry.try_borrow(id)));
1362
1363        for (name, entry_point) in registry_entry_points() {
1364            let error = entry_point(&id).unwrap_err();
1365            assert!(
1366                error
1367                    .to_string()
1368                    .starts_with("Component 'borrowed-component' is already mutably borrowed."),
1369                "unexpected error from {name}: {error}"
1370            );
1371        }
1372
1373        let error = component_state(&id).unwrap_err();
1374        assert!(
1375            error
1376                .to_string()
1377                .starts_with("Component 'borrowed-component' is already mutably borrowed.")
1378        );
1379
1380        // The rejected calls must leave the original borrow in place
1381        assert!(with_component_registry(|registry| registry.is_borrowed(&id)));
1382    }
1383
1384    type RegistryEntryPoint = (&'static str, fn(&Ustr) -> anyhow::Result<()>);
1385
1386    /// Lifecycle entry points that resolve a component from the global registry.
1387    fn registry_entry_points() -> [RegistryEntryPoint; 5] {
1388        [
1389            ("start", start_component),
1390            ("stop", stop_component),
1391            ("reset", reset_component),
1392            ("dispose", dispose_component),
1393            ("release", release_component_subscriptions),
1394        ]
1395    }
1396
1397    #[rstest]
1398    fn test_registry_entry_points_drive_the_full_lifecycle() {
1399        clear_component_registry();
1400
1401        let id = TestComponent::new("lifecycle-component", &NO_PANIC).register_in_global_registry();
1402
1403        assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1404
1405        start_component(&id).unwrap();
1406        assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
1407
1408        stop_component(&id).unwrap();
1409        assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
1410
1411        reset_component(&id).unwrap();
1412        assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1413
1414        dispose_component(&id).unwrap();
1415        assert_eq!(component_state(&id).unwrap(), ComponentState::Disposed);
1416
1417        assert!(!with_component_registry(
1418            |registry| registry.is_borrowed(&id)
1419        ));
1420    }
1421
1422    #[rstest]
1423    fn test_release_component_subscriptions_invokes_the_component_hook() {
1424        clear_component_registry();
1425
1426        let component = TestComponent::new("releasing-component", &NO_PANIC);
1427        let id = component.id.inner();
1428        let component_ref = Rc::new(UnsafeCell::new(component));
1429        let observed = component_ref.clone();
1430        with_component_registry(|registry| {
1431            registry.insert(id, component_ref as Rc<UnsafeCell<dyn Component>>);
1432        });
1433
1434        release_component_subscriptions(&id).unwrap();
1435        release_component_subscriptions(&id).unwrap();
1436
1437        // SAFETY: no lifecycle call is in flight, so no other reference exists
1438        assert_eq!(unsafe { (*observed.get()).releases }, 2);
1439    }
1440
1441    #[rstest]
1442    fn test_deregister_component_removes_only_the_named_component() {
1443        clear_component_registry();
1444
1445        let kept = TestComponent::new("kept-component", &NO_PANIC).register_in_global_registry();
1446        let removed =
1447            TestComponent::new("removed-component", &NO_PANIC).register_in_global_registry();
1448
1449        deregister_component(&removed);
1450
1451        assert!(get_component(&removed).is_none());
1452        assert!(get_component(&kept).is_some());
1453        assert_eq!(component_state(&kept).unwrap(), ComponentState::Ready);
1454    }
1455
1456    #[rstest]
1457    fn test_component_registry_debug_reports_components_and_borrows() {
1458        clear_component_registry();
1459
1460        let id = TestComponent::new("debug-component", &NO_PANIC).register_in_global_registry();
1461        assert!(with_component_registry(|registry| registry.try_borrow(id)));
1462
1463        let debug = with_component_registry(|registry| format!("{registry:?}"));
1464
1465        assert!(debug.contains("debug-component"), "{debug}");
1466        assert!(debug.contains("active_borrows: 1"), "{debug}");
1467    }
1468
1469    #[rstest]
1470    fn test_registry_remove_returns_the_registered_component() {
1471        clear_component_registry();
1472
1473        let id = TestComponent::new("removable", &NO_PANIC).register_in_global_registry();
1474
1475        assert!(with_component_registry(|registry| registry.remove(&id)).is_some());
1476        assert!(with_component_registry(|registry| registry.remove(&id)).is_none());
1477    }
1478
1479    #[rstest]
1480    #[case("start", ComponentState::Ready, ComponentState::Starting, 0)]
1481    #[case("stop", ComponentState::Running, ComponentState::Stopping, 0)]
1482    #[case("resume", ComponentState::Stopped, ComponentState::Resuming, 0)]
1483    #[case("degrade", ComponentState::Running, ComponentState::Degrading, 0)]
1484    #[case("fault", ComponentState::Running, ComponentState::Faulting, 1)]
1485    #[case("reset", ComponentState::Ready, ComponentState::Resetting, 0)]
1486    #[case("dispose", ComponentState::Ready, ComponentState::Faulted, 0)]
1487    fn test_lifecycle_error_returns_without_logging(
1488        #[case] method: &str,
1489        #[case] initial: ComponentState,
1490        #[case] expected: ComponentState,
1491        #[case] releases: usize,
1492    ) {
1493        let _guard = init_logging(
1494            TraderId::from("TRADER-001"),
1495            UUID4::new(),
1496            Default::default(),
1497            Default::default(),
1498        )
1499        .unwrap();
1500        let mut component = TestComponent::new("failing-hook", &NO_PANIC);
1501        component.state = initial;
1502        component.hooks_fail = true;
1503        arm_shutdown_on_error(true);
1504
1505        let result = match method {
1506            "start" => component.start(),
1507            "stop" => component.stop(),
1508            "resume" => component.resume(),
1509            "degrade" => component.degrade(),
1510            "fault" => component.fault(),
1511            "reset" => component.reset(),
1512            "dispose" => component.dispose(),
1513            _ => unreachable!(),
1514        };
1515
1516        let trigger = take_shutdown_on_error_trigger();
1517        disarm_shutdown_on_error();
1518
1519        assert_eq!(
1520            result.unwrap_err().to_string(),
1521            format!("on_{method} failed")
1522        );
1523        assert_eq!(component.state(), expected);
1524        assert_eq!(component.releases, releases);
1525        assert_eq!(trigger, None);
1526    }
1527}