1#![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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
47pub enum ComponentAccessError {
48 #[error("Cannot access {resource} during {operation}: the actor is not registered")]
50 NotRegistered {
51 resource: &'static str,
53 operation: &'static str,
55 },
56 #[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 resource: &'static str,
63 operation: &'static str,
65 },
66 #[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 resource: &'static str,
73 operation: &'static str,
75 },
76}
77
78pub trait Component {
80 fn component_id(&self) -> ComponentId;
82
83 fn state(&self) -> ComponentState;
85
86 fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()>;
92
93 fn is_ready(&self) -> bool {
95 self.state() == ComponentState::Ready
96 }
97
98 fn not_running(&self) -> bool {
100 !self.is_running()
101 }
102
103 fn is_running(&self) -> bool {
105 self.state() == ComponentState::Running
106 }
107
108 fn is_stopped(&self) -> bool {
110 self.state() == ComponentState::Stopped
111 }
112
113 fn is_degraded(&self) -> bool {
115 self.state() == ComponentState::Degraded
116 }
117
118 fn is_faulted(&self) -> bool {
120 self.state() == ComponentState::Faulted
121 }
122
123 fn is_disposed(&self) -> bool {
125 self.state() == ComponentState::Disposed
126 }
127
128 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 fn initialize(&mut self) -> anyhow::Result<()> {
146 self.transition_state(ComponentTrigger::Initialize)
147 }
148
149 fn start(&mut self) -> anyhow::Result<()> {
155 self.transition_state(ComponentTrigger::Start)?; self.on_start()?;
158
159 self.transition_state(ComponentTrigger::StartCompleted)?;
160
161 Ok(())
162 }
163
164 fn stop(&mut self) -> anyhow::Result<()> {
170 self.transition_state(ComponentTrigger::Stop)?; self.on_stop()?;
173
174 self.transition_state(ComponentTrigger::StopCompleted)?;
175
176 Ok(())
177 }
178
179 fn resume(&mut self) -> anyhow::Result<()> {
185 self.transition_state(ComponentTrigger::Resume)?; self.on_resume()?;
188
189 self.transition_state(ComponentTrigger::ResumeCompleted)?;
190
191 Ok(())
192 }
193
194 fn degrade(&mut self) -> anyhow::Result<()> {
200 self.transition_state(ComponentTrigger::Degrade)?; self.on_degrade()?;
203
204 self.transition_state(ComponentTrigger::DegradeCompleted)?;
205
206 Ok(())
207 }
208
209 fn fault(&mut self) -> anyhow::Result<()> {
221 self.transition_state(ComponentTrigger::Fault)?; 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 fn reset(&mut self) -> anyhow::Result<()> {
245 self.transition_state(ComponentTrigger::Reset)?; self.on_reset()?;
248
249 self.release_subscriptions();
250 self.transition_state(ComponentTrigger::ResetCompleted)?;
251
252 Ok(())
253 }
254
255 fn dispose(&mut self) -> anyhow::Result<()> {
271 self.transition_state(ComponentTrigger::Dispose)?; if let Err(e) = self.on_dispose() {
274 self.transition_state(ComponentTrigger::Fault)?; self.transition_state(ComponentTrigger::FaultCompleted)?; return Err(e);
278 }
279
280 self.release_subscriptions();
281 self.transition_state(ComponentTrigger::DisposeCompleted)?;
282
283 Ok(())
284 }
285
286 fn release_subscriptions(&mut self) {}
292
293 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 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 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 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 fn on_dispose(&mut self) -> anyhow::Result<()> {
355 Ok(())
356 }
357
358 fn on_degrade(&mut self) -> anyhow::Result<()> {
364 Ok(())
365 }
366
367 fn on_fault(&mut self) -> anyhow::Result<()> {
373 Ok(())
374 }
375}
376
377#[rustfmt::skip]
378impl ComponentState {
379 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
425pub 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 pub fn remove(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
469 self.components.borrow_mut().remove(id)
470 }
471
472 pub fn is_borrowed(&self, id: &Ustr) -> bool {
474 self.borrows.borrow().contains(id)
475 }
476
477 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 fn release_borrow(&self, id: &Ustr) {
490 self.borrows.borrow_mut().remove(id);
491 }
492}
493
494struct 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
518pub 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 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
533pub 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 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 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
553pub 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 unsafe {
580 let component = &mut *component_ref.get();
581 component.start()
582 }
583}
584
585pub 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 unsafe {
611 let component = &*component_ref.get();
612 Ok(component.state())
613 }
614}
615
616pub 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 unsafe {
643 let component = &mut *component_ref.get();
644 component.stop()
645 }
646}
647
648pub 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 unsafe {
675 let component = &mut *component_ref.get();
676 component.reset()
677 }
678}
679
680pub 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 unsafe {
707 let component = &mut *component_ref.get();
708 component.dispose()
709 }
710}
711
712pub 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 unsafe {
741 let component = &mut *component_ref.get();
742 component.release_subscriptions();
743 }
744
745 Ok(())
746}
747
748pub fn get_component(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
750 with_component_registry(|registry| registry.get(id))
751}
752
753pub fn deregister_component(id: &Ustr) {
758 with_component_registry(|registry| registry.remove(id));
759}
760
761#[cfg(test)]
762pub 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)] 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 start_component(&id).unwrap();
932 assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
933
934 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 let _ = start_component(&id);
952
953 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 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 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 #[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 assert!(with_component_registry(|registry| registry.is_borrowed(&id)));
1376 }
1377
1378 type RegistryEntryPoint = (&'static str, fn(&Ustr) -> anyhow::Result<()>);
1379
1380 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 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}