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)?; if let Err(e) = self.on_start() {
158 log_error(self.component_id(), &e);
159 return Err(e); }
161
162 self.transition_state(ComponentTrigger::StartCompleted)?;
163
164 Ok(())
165 }
166
167 fn stop(&mut self) -> anyhow::Result<()> {
173 self.transition_state(ComponentTrigger::Stop)?; if let Err(e) = self.on_stop() {
176 log_error(self.component_id(), &e);
177 return Err(e); }
179
180 self.transition_state(ComponentTrigger::StopCompleted)?;
181
182 Ok(())
183 }
184
185 fn resume(&mut self) -> anyhow::Result<()> {
191 self.transition_state(ComponentTrigger::Resume)?; if let Err(e) = self.on_resume() {
194 log_error(self.component_id(), &e);
195 return Err(e); }
197
198 self.transition_state(ComponentTrigger::ResumeCompleted)?;
199
200 Ok(())
201 }
202
203 fn degrade(&mut self) -> anyhow::Result<()> {
209 self.transition_state(ComponentTrigger::Degrade)?; if let Err(e) = self.on_degrade() {
212 log_error(self.component_id(), &e);
213 return Err(e); }
215
216 self.transition_state(ComponentTrigger::DegradeCompleted)?;
217
218 Ok(())
219 }
220
221 fn fault(&mut self) -> anyhow::Result<()> {
233 self.transition_state(ComponentTrigger::Fault)?; let result = self.on_fault();
236 self.release_subscriptions();
237
238 if let Err(e) = result {
239 log_error(self.component_id(), &e);
240 return Err(e); }
242
243 self.transition_state(ComponentTrigger::FaultCompleted)?;
244
245 Ok(())
246 }
247
248 fn reset(&mut self) -> anyhow::Result<()> {
260 self.transition_state(ComponentTrigger::Reset)?; if let Err(e) = self.on_reset() {
263 log_error(self.component_id(), &e);
264 return Err(e); }
266
267 self.release_subscriptions();
268 self.transition_state(ComponentTrigger::ResetCompleted)?;
269
270 Ok(())
271 }
272
273 fn dispose(&mut self) -> anyhow::Result<()> {
289 self.transition_state(ComponentTrigger::Dispose)?; if let Err(e) = self.on_dispose() {
292 log_error(self.component_id(), &e);
293
294 self.transition_state(ComponentTrigger::Fault)?; self.transition_state(ComponentTrigger::FaultCompleted)?; return Err(e);
298 }
299
300 self.release_subscriptions();
301 self.transition_state(ComponentTrigger::DisposeCompleted)?;
302
303 Ok(())
304 }
305
306 fn release_subscriptions(&mut self) {}
312
313 fn on_start(&mut self) -> anyhow::Result<()> {
319 log::warn!(
320 "The `on_start` handler was called when not overridden, \
321 it's expected that any actions required when stopping the component \
322 occur here, such as unsubscribing from data",
323 );
324 Ok(())
325 }
326
327 fn on_stop(&mut self) -> anyhow::Result<()> {
333 log::warn!(
334 "The `on_stop` handler was called when not overridden, \
335 it's expected that any actions required when stopping the component \
336 occur here, such as unsubscribing from data",
337 );
338 Ok(())
339 }
340
341 fn on_resume(&mut self) -> anyhow::Result<()> {
347 log::warn!(
348 "The `on_resume` handler was called when not overridden, \
349 it's expected that any actions required when resuming the component \
350 following a stop occur here"
351 );
352 Ok(())
353 }
354
355 fn on_reset(&mut self) -> anyhow::Result<()> {
361 log::warn!(
362 "The `on_reset` handler was called when not overridden, \
363 it's expected that any actions required when resetting the component \
364 occur here, such as resetting indicators and other state"
365 );
366 Ok(())
367 }
368
369 fn on_dispose(&mut self) -> anyhow::Result<()> {
375 Ok(())
376 }
377
378 fn on_degrade(&mut self) -> anyhow::Result<()> {
384 Ok(())
385 }
386
387 fn on_fault(&mut self) -> anyhow::Result<()> {
393 Ok(())
394 }
395}
396
397fn log_error(component: ComponentId, e: &anyhow::Error) {
398 log::error!(component = component.as_str(); "{e}");
399}
400
401#[rustfmt::skip]
402impl ComponentState {
403 pub fn transition(&mut self, trigger: &ComponentTrigger) -> anyhow::Result<Self> {
409 let new_state = match (&self, trigger) {
410 (Self::PreInitialized, ComponentTrigger::Initialize) => Self::Ready,
411 (Self::Ready, ComponentTrigger::Reset) => Self::Resetting,
412 (Self::Ready, ComponentTrigger::Start) => Self::Starting,
413 (Self::Ready, ComponentTrigger::Dispose) => Self::Disposing,
414 (Self::Resetting, ComponentTrigger::ResetCompleted) => Self::Ready,
415 (Self::Starting, ComponentTrigger::StartCompleted) => Self::Running,
416 (Self::Starting, ComponentTrigger::Stop) => Self::Stopping,
417 (Self::Starting, ComponentTrigger::Fault) => Self::Faulting,
418 (Self::Running, ComponentTrigger::Stop) => Self::Stopping,
419 (Self::Running, ComponentTrigger::Degrade) => Self::Degrading,
420 (Self::Running, ComponentTrigger::Fault) => Self::Faulting,
421 (Self::Resuming, ComponentTrigger::Stop) => Self::Stopping,
422 (Self::Resuming, ComponentTrigger::ResumeCompleted) => Self::Running,
423 (Self::Resuming, ComponentTrigger::Fault) => Self::Faulting,
424 (Self::Stopping, ComponentTrigger::StopCompleted) => Self::Stopped,
425 (Self::Stopping, ComponentTrigger::Dispose) => Self::Disposing,
426 (Self::Stopping, ComponentTrigger::Fault) => Self::Faulting,
427 (Self::Stopped, ComponentTrigger::Reset) => Self::Resetting,
428 (Self::Stopped, ComponentTrigger::Resume) => Self::Resuming,
429 (Self::Stopped, ComponentTrigger::Dispose) => Self::Disposing,
430 (Self::Stopped, ComponentTrigger::Fault) => Self::Faulting,
431 (Self::Degrading, ComponentTrigger::DegradeCompleted) => Self::Degraded,
432 (Self::Degraded, ComponentTrigger::Resume) => Self::Resuming,
433 (Self::Degraded, ComponentTrigger::Stop) => Self::Stopping,
434 (Self::Degraded, ComponentTrigger::Fault) => Self::Faulting,
435 (Self::Disposing, ComponentTrigger::DisposeCompleted) => Self::Disposed,
436 (Self::Disposing, ComponentTrigger::Fault) => Self::Faulting,
437 (Self::Faulting, ComponentTrigger::Dispose) => Self::Disposing,
438 (Self::Faulting, ComponentTrigger::FaultCompleted) => Self::Faulted,
439 _ => anyhow::bail!("Invalid state trigger {self} -> {trigger}"),
440 };
441 Ok(new_state)
442 }
443}
444
445thread_local! {
446 static COMPONENT_REGISTRY: ComponentRegistry = ComponentRegistry::new();
447}
448
449pub struct ComponentRegistry {
454 components: RefCell<AHashMap<Ustr, Rc<UnsafeCell<dyn Component>>>>,
455 borrows: RefCell<AHashSet<Ustr>>,
456}
457
458impl Debug for ComponentRegistry {
459 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 let components_ref = self.components.borrow();
461 let keys: Vec<&Ustr> = components_ref.keys().collect();
462 f.debug_struct(stringify!(ComponentRegistry))
463 .field("components", &keys)
464 .field("active_borrows", &self.borrows.borrow().len())
465 .finish()
466 }
467}
468
469impl Default for ComponentRegistry {
470 fn default() -> Self {
471 Self::new()
472 }
473}
474
475impl ComponentRegistry {
476 pub fn new() -> Self {
477 Self {
478 components: RefCell::new(AHashMap::new()),
479 borrows: RefCell::new(AHashSet::new()),
480 }
481 }
482
483 pub fn insert(&self, id: Ustr, component: Rc<UnsafeCell<dyn Component>>) {
484 self.components.borrow_mut().insert(id, component);
485 }
486
487 pub fn get(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
488 self.components.borrow().get(id).cloned()
489 }
490
491 pub fn remove(&self, id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
493 self.components.borrow_mut().remove(id)
494 }
495
496 pub fn is_borrowed(&self, id: &Ustr) -> bool {
498 self.borrows.borrow().contains(id)
499 }
500
501 fn try_borrow(&self, id: Ustr) -> bool {
503 let mut borrows = self.borrows.borrow_mut();
504 if borrows.contains(&id) {
505 false
506 } else {
507 borrows.insert(id);
508 true
509 }
510 }
511
512 fn release_borrow(&self, id: &Ustr) {
514 self.borrows.borrow_mut().remove(id);
515 }
516}
517
518struct BorrowGuard {
523 id: Ustr,
524}
525
526impl BorrowGuard {
527 fn new(id: Ustr) -> Self {
528 Self { id }
529 }
530}
531
532impl Drop for BorrowGuard {
533 fn drop(&mut self) {
534 with_component_registry(|registry| registry.release_borrow(&self.id));
535 }
536}
537
538pub fn with_component_registry<R>(f: impl FnOnce(&ComponentRegistry) -> R) -> R {
539 COMPONENT_REGISTRY.with(f)
540}
541
542pub fn register_component<T>(component: T) -> Rc<UnsafeCell<T>>
544where
545 T: Component + 'static,
546{
547 let component_id = component.component_id().inner();
548 let component_ref = Rc::new(UnsafeCell::new(component));
549
550 let component_trait_ref: Rc<UnsafeCell<dyn Component>> = component_ref.clone();
552 with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
553
554 component_ref
555}
556
557pub fn register_component_actor<T>(component: T) -> Rc<UnsafeCell<T>>
559where
560 T: Component + Actor + 'static,
561{
562 let component_id = component.component_id().inner();
563 let actor_id = component.id();
564 let component_ref = Rc::new(UnsafeCell::new(component));
565
566 let component_trait_ref: Rc<UnsafeCell<dyn Component>> = component_ref.clone();
568 with_component_registry(|registry| registry.insert(component_id, component_trait_ref));
569
570 let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = component_ref.clone();
572 with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
573
574 component_ref
575}
576
577pub fn start_component(id: &Ustr) -> anyhow::Result<()> {
585 let component_ref = with_component_registry(|registry| {
586 let component_ref = registry
587 .get(id)
588 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
589
590 if !registry.try_borrow(*id) {
591 anyhow::bail!(
592 "Component '{id}' is already mutably borrowed. \
593 This would create aliasing mutable references (undefined behavior)."
594 );
595 }
596
597 Ok::<_, anyhow::Error>(component_ref)
598 })?;
599
600 let _guard = BorrowGuard::new(*id);
601
602 unsafe {
604 let component = &mut *component_ref.get();
605 component.start()
606 }
607}
608
609pub fn component_state(id: &Ustr) -> anyhow::Result<ComponentState> {
616 let component_ref = with_component_registry(|registry| {
617 let component_ref = registry
618 .get(id)
619 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
620
621 if !registry.try_borrow(*id) {
622 anyhow::bail!(
623 "Component '{id}' is already mutably borrowed. \
624 This would create aliasing mutable references (undefined behavior)."
625 );
626 }
627
628 Ok::<_, anyhow::Error>(component_ref)
629 })?;
630
631 let _guard = BorrowGuard::new(*id);
632
633 unsafe {
635 let component = &*component_ref.get();
636 Ok(component.state())
637 }
638}
639
640pub fn stop_component(id: &Ustr) -> anyhow::Result<()> {
648 let component_ref = with_component_registry(|registry| {
649 let component_ref = registry
650 .get(id)
651 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
652
653 if !registry.try_borrow(*id) {
654 anyhow::bail!(
655 "Component '{id}' is already mutably borrowed. \
656 This would create aliasing mutable references (undefined behavior)."
657 );
658 }
659
660 Ok::<_, anyhow::Error>(component_ref)
661 })?;
662
663 let _guard = BorrowGuard::new(*id);
664
665 unsafe {
667 let component = &mut *component_ref.get();
668 component.stop()
669 }
670}
671
672pub fn reset_component(id: &Ustr) -> anyhow::Result<()> {
680 let component_ref = with_component_registry(|registry| {
681 let component_ref = registry
682 .get(id)
683 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
684
685 if !registry.try_borrow(*id) {
686 anyhow::bail!(
687 "Component '{id}' is already mutably borrowed. \
688 This would create aliasing mutable references (undefined behavior)."
689 );
690 }
691
692 Ok::<_, anyhow::Error>(component_ref)
693 })?;
694
695 let _guard = BorrowGuard::new(*id);
696
697 unsafe {
699 let component = &mut *component_ref.get();
700 component.reset()
701 }
702}
703
704pub fn dispose_component(id: &Ustr) -> anyhow::Result<()> {
712 let component_ref = with_component_registry(|registry| {
713 let component_ref = registry
714 .get(id)
715 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
716
717 if !registry.try_borrow(*id) {
718 anyhow::bail!(
719 "Component '{id}' is already mutably borrowed. \
720 This would create aliasing mutable references (undefined behavior)."
721 );
722 }
723
724 Ok::<_, anyhow::Error>(component_ref)
725 })?;
726
727 let _guard = BorrowGuard::new(*id);
728
729 unsafe {
731 let component = &mut *component_ref.get();
732 component.dispose()
733 }
734}
735
736pub fn release_component_subscriptions(id: &Ustr) -> anyhow::Result<()> {
746 let component_ref = with_component_registry(|registry| {
747 let component_ref = registry
748 .get(id)
749 .ok_or_else(|| anyhow::anyhow!("Component '{id}' not found in global registry"))?;
750
751 if !registry.try_borrow(*id) {
752 anyhow::bail!(
753 "Component '{id}' is already mutably borrowed. \
754 This would create aliasing mutable references (undefined behavior)."
755 );
756 }
757
758 Ok::<_, anyhow::Error>(component_ref)
759 })?;
760
761 let _guard = BorrowGuard::new(*id);
762
763 unsafe {
765 let component = &mut *component_ref.get();
766 component.release_subscriptions();
767 }
768
769 Ok(())
770}
771
772pub fn get_component(id: &Ustr) -> Option<Rc<UnsafeCell<dyn Component>>> {
774 with_component_registry(|registry| registry.get(id))
775}
776
777pub fn deregister_component(id: &Ustr) {
782 with_component_registry(|registry| registry.remove(id));
783}
784
785#[cfg(test)]
786pub fn clear_component_registry() {
788 with_component_registry(|registry| {
789 registry.components.borrow_mut().clear();
790 registry.borrows.borrow_mut().clear();
791 });
792}
793
794#[cfg(test)]
795mod tests {
796 use std::{
797 any::Any,
798 sync::atomic::{AtomicBool, Ordering},
799 };
800
801 use rstest::rstest;
802
803 use super::*;
804
805 #[derive(Debug)]
806 struct TestComponent {
807 id: ComponentId,
808 state: ComponentState,
809 should_panic: &'static AtomicBool,
810 hooks_fail: bool,
811 releases: usize,
812 }
813
814 impl TestComponent {
815 fn new(name: &str, should_panic: &'static AtomicBool) -> Self {
816 Self {
817 id: ComponentId::new(name),
818 state: ComponentState::Ready,
819 should_panic,
820 hooks_fail: false,
821 releases: 0,
822 }
823 }
824
825 fn register_in_global_registry(self) -> Ustr {
826 let id = self.id.inner();
827 let component_ref: Rc<UnsafeCell<dyn Component>> = Rc::new(UnsafeCell::new(self));
828 with_component_registry(|registry| registry.insert(id, component_ref));
829 id
830 }
831 }
832
833 impl Actor for TestComponent {
834 fn id(&self) -> Ustr {
835 self.id.inner()
836 }
837
838 fn handle(&mut self, _msg: &dyn Any) {}
839
840 fn as_any(&self) -> &dyn Any {
841 self
842 }
843 }
844
845 impl Component for TestComponent {
846 fn component_id(&self) -> ComponentId {
847 self.id
848 }
849
850 fn state(&self) -> ComponentState {
851 self.state
852 }
853
854 fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()> {
855 self.state = self.state.transition(&trigger)?;
856 Ok(())
857 }
858
859 fn register(
860 &mut self,
861 _trader_id: TraderId,
862 _clock: Rc<RefCell<dyn Clock>>,
863 _cache: Rc<RefCell<Cache>>,
864 ) -> anyhow::Result<()> {
865 Ok(())
866 }
867
868 #[expect(clippy::panic_in_result_fn)] fn on_start(&mut self) -> anyhow::Result<()> {
870 assert!(
871 !self.should_panic.load(Ordering::SeqCst),
872 "Intentional panic for testing"
873 );
874
875 if self.hooks_fail {
876 anyhow::bail!("on_start failed");
877 }
878
879 Ok(())
880 }
881
882 fn on_stop(&mut self) -> anyhow::Result<()> {
883 if self.hooks_fail {
884 anyhow::bail!("on_stop failed");
885 }
886
887 Ok(())
888 }
889
890 fn on_resume(&mut self) -> anyhow::Result<()> {
891 if self.hooks_fail {
892 anyhow::bail!("on_resume failed");
893 }
894
895 Ok(())
896 }
897
898 fn on_reset(&mut self) -> anyhow::Result<()> {
899 if self.hooks_fail {
900 anyhow::bail!("on_reset failed");
901 }
902
903 Ok(())
904 }
905
906 fn on_dispose(&mut self) -> anyhow::Result<()> {
907 if self.hooks_fail {
908 anyhow::bail!("on_dispose failed");
909 }
910
911 Ok(())
912 }
913
914 fn on_fault(&mut self) -> anyhow::Result<()> {
915 if self.hooks_fail {
916 anyhow::bail!("on_fault failed");
917 }
918
919 Ok(())
920 }
921
922 fn on_degrade(&mut self) -> anyhow::Result<()> {
923 if self.hooks_fail {
924 anyhow::bail!("on_degrade failed");
925 }
926
927 Ok(())
928 }
929
930 fn release_subscriptions(&mut self) {
931 self.releases += 1;
932 }
933 }
934
935 static NO_PANIC: AtomicBool = AtomicBool::new(false);
936 static DO_PANIC: AtomicBool = AtomicBool::new(true);
937
938 #[rstest]
939 fn test_component_borrow_tracking_prevents_double_borrow() {
940 clear_component_registry();
941
942 let id = Ustr::from("test-component-1");
943 let component = TestComponent::new("test-component-1", &NO_PANIC);
944 let component_id = component.id.inner();
945
946 let component_ref = Rc::new(UnsafeCell::new(component));
947 with_component_registry(|registry| registry.insert(component_id, component_ref));
948
949 start_component(&id).unwrap();
951 assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
952
953 stop_component(&id).unwrap();
955 assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
956 }
957
958 #[rstest]
959 fn test_component_borrow_released_after_lifecycle_call() {
960 clear_component_registry();
961
962 let id = Ustr::from("test-component-2");
963 let component = TestComponent::new("test-component-2", &NO_PANIC);
964 let component_id = component.id.inner();
965
966 let component_ref = Rc::new(UnsafeCell::new(component));
967 with_component_registry(|registry| registry.insert(component_id, component_ref));
968
969 let _ = start_component(&id);
971
972 assert!(!with_component_registry(
974 |registry| registry.is_borrowed(&id)
975 ));
976 }
977
978 #[rstest]
979 fn test_component_borrow_released_on_panic() {
980 clear_component_registry();
981
982 let id = Ustr::from("test-component-panic");
983 let component = TestComponent::new("test-component-panic", &DO_PANIC);
984 let component_id = component.id.inner();
985
986 let component_ref = Rc::new(UnsafeCell::new(component));
987 with_component_registry(|registry| registry.insert(component_id, component_ref));
988
989 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
991 let _ = start_component(&id);
992 }));
993 assert!(result.is_err(), "Expected panic from on_start");
994
995 assert!(
997 !with_component_registry(|registry| registry.is_borrowed(&id)),
998 "Borrow was not released after panic"
999 );
1000 }
1001
1002 #[rstest]
1003 #[case(ComponentState::PreInitialized, ComponentTrigger::Start)]
1004 #[case(ComponentState::Ready, ComponentTrigger::Resume)]
1005 #[case(ComponentState::Running, ComponentTrigger::Start)]
1006 #[case(ComponentState::Stopped, ComponentTrigger::Stop)]
1007 #[case(ComponentState::Disposed, ComponentTrigger::Dispose)]
1008 #[case(ComponentState::Faulted, ComponentTrigger::Fault)]
1009 fn test_transition_rejects_invalid_trigger(
1010 #[case] state: ComponentState,
1011 #[case] trigger: ComponentTrigger,
1012 ) {
1013 let mut current = state;
1014
1015 let error = current.transition(&trigger).unwrap_err();
1016
1017 assert_eq!(
1018 error.to_string(),
1019 format!("Invalid state trigger {state} -> {trigger}")
1020 );
1021 assert_eq!(current, state, "Rejected trigger must not mutate the state");
1022 }
1023
1024 #[rstest]
1026 #[case(
1027 ComponentState::PreInitialized,
1028 ComponentTrigger::Initialize,
1029 ComponentState::Ready
1030 )]
1031 #[case(
1032 ComponentState::Ready,
1033 ComponentTrigger::Reset,
1034 ComponentState::Resetting
1035 )]
1036 #[case(
1037 ComponentState::Ready,
1038 ComponentTrigger::Start,
1039 ComponentState::Starting
1040 )]
1041 #[case(
1042 ComponentState::Ready,
1043 ComponentTrigger::Dispose,
1044 ComponentState::Disposing
1045 )]
1046 #[case(
1047 ComponentState::Resetting,
1048 ComponentTrigger::ResetCompleted,
1049 ComponentState::Ready
1050 )]
1051 #[case(
1052 ComponentState::Starting,
1053 ComponentTrigger::StartCompleted,
1054 ComponentState::Running
1055 )]
1056 #[case(
1057 ComponentState::Starting,
1058 ComponentTrigger::Stop,
1059 ComponentState::Stopping
1060 )]
1061 #[case(
1062 ComponentState::Starting,
1063 ComponentTrigger::Fault,
1064 ComponentState::Faulting
1065 )]
1066 #[case(
1067 ComponentState::Running,
1068 ComponentTrigger::Stop,
1069 ComponentState::Stopping
1070 )]
1071 #[case(
1072 ComponentState::Running,
1073 ComponentTrigger::Degrade,
1074 ComponentState::Degrading
1075 )]
1076 #[case(
1077 ComponentState::Running,
1078 ComponentTrigger::Fault,
1079 ComponentState::Faulting
1080 )]
1081 #[case(
1082 ComponentState::Resuming,
1083 ComponentTrigger::Stop,
1084 ComponentState::Stopping
1085 )]
1086 #[case(
1087 ComponentState::Resuming,
1088 ComponentTrigger::ResumeCompleted,
1089 ComponentState::Running
1090 )]
1091 #[case(
1092 ComponentState::Resuming,
1093 ComponentTrigger::Fault,
1094 ComponentState::Faulting
1095 )]
1096 #[case(
1097 ComponentState::Stopping,
1098 ComponentTrigger::StopCompleted,
1099 ComponentState::Stopped
1100 )]
1101 #[case(
1102 ComponentState::Stopping,
1103 ComponentTrigger::Dispose,
1104 ComponentState::Disposing
1105 )]
1106 #[case(
1107 ComponentState::Stopping,
1108 ComponentTrigger::Fault,
1109 ComponentState::Faulting
1110 )]
1111 #[case(
1112 ComponentState::Stopped,
1113 ComponentTrigger::Reset,
1114 ComponentState::Resetting
1115 )]
1116 #[case(
1117 ComponentState::Stopped,
1118 ComponentTrigger::Resume,
1119 ComponentState::Resuming
1120 )]
1121 #[case(
1122 ComponentState::Stopped,
1123 ComponentTrigger::Dispose,
1124 ComponentState::Disposing
1125 )]
1126 #[case(
1127 ComponentState::Stopped,
1128 ComponentTrigger::Fault,
1129 ComponentState::Faulting
1130 )]
1131 #[case(
1132 ComponentState::Degrading,
1133 ComponentTrigger::DegradeCompleted,
1134 ComponentState::Degraded
1135 )]
1136 #[case(
1137 ComponentState::Degraded,
1138 ComponentTrigger::Resume,
1139 ComponentState::Resuming
1140 )]
1141 #[case(
1142 ComponentState::Degraded,
1143 ComponentTrigger::Stop,
1144 ComponentState::Stopping
1145 )]
1146 #[case(
1147 ComponentState::Degraded,
1148 ComponentTrigger::Fault,
1149 ComponentState::Faulting
1150 )]
1151 #[case(
1152 ComponentState::Disposing,
1153 ComponentTrigger::DisposeCompleted,
1154 ComponentState::Disposed
1155 )]
1156 #[case(
1157 ComponentState::Disposing,
1158 ComponentTrigger::Fault,
1159 ComponentState::Faulting
1160 )]
1161 #[case(
1162 ComponentState::Faulting,
1163 ComponentTrigger::Dispose,
1164 ComponentState::Disposing
1165 )]
1166 #[case(
1167 ComponentState::Faulting,
1168 ComponentTrigger::FaultCompleted,
1169 ComponentState::Faulted
1170 )]
1171 fn test_transition_accepts_valid_trigger(
1172 #[case] state: ComponentState,
1173 #[case] trigger: ComponentTrigger,
1174 #[case] expected: ComponentState,
1175 ) {
1176 let mut current = state;
1177
1178 assert_eq!(current.transition(&trigger).unwrap(), expected);
1179 }
1180
1181 #[rstest]
1182 fn test_state_predicates_match_the_current_state() {
1183 let mut component = TestComponent::new("predicates", &NO_PANIC);
1184
1185 assert!(component.is_ready());
1186 assert!(component.not_running());
1187
1188 component.start().unwrap();
1189 assert!(component.is_running());
1190 assert!(!component.not_running());
1191 assert!(!component.is_ready());
1192
1193 component.stop().unwrap();
1194 assert!(component.is_stopped());
1195 assert!(!component.is_running());
1196
1197 component.resume().unwrap();
1198 component.degrade().unwrap();
1199 assert!(component.is_degraded());
1200 assert!(!component.is_stopped());
1201
1202 component.fault().unwrap();
1203 assert!(component.is_faulted());
1204 assert!(!component.is_degraded());
1205 assert!(!component.is_disposed());
1206 }
1207
1208 #[rstest]
1209 fn test_is_disposed_only_after_disposal() {
1210 let mut component = TestComponent::new("disposable", &NO_PANIC);
1211
1212 assert!(!component.is_disposed());
1213
1214 component.dispose().unwrap();
1215
1216 assert!(component.is_disposed());
1217 assert!(!component.is_ready());
1218 assert!(!component.is_faulted());
1219 }
1220
1221 #[rstest]
1222 fn test_degrade_halts_transition_when_on_degrade_fails() {
1223 let mut component = TestComponent::new("failing-degrade", &NO_PANIC);
1224 component.start().unwrap();
1225 component.hooks_fail = true;
1226
1227 let error = component.degrade().unwrap_err();
1228
1229 assert_eq!(error.to_string(), "on_degrade failed");
1230 assert_eq!(component.state(), ComponentState::Degrading);
1231 }
1232
1233 #[rstest]
1234 fn test_initialize_advances_from_pre_initialized() {
1235 let mut component = TestComponent::new("initializing", &NO_PANIC);
1236 component.state = ComponentState::PreInitialized;
1237
1238 component.initialize().unwrap();
1239
1240 assert_eq!(component.state(), ComponentState::Ready);
1241 }
1242
1243 #[rstest]
1244 fn test_start_halts_transition_when_on_start_fails() {
1245 let mut component = TestComponent::new("failing-start", &NO_PANIC);
1246 component.hooks_fail = true;
1247
1248 let error = component.start().unwrap_err();
1249
1250 assert_eq!(error.to_string(), "on_start failed");
1251 assert_eq!(component.state(), ComponentState::Starting);
1252 }
1253
1254 #[rstest]
1255 fn test_stop_halts_transition_when_on_stop_fails() {
1256 let mut component = TestComponent::new("failing-stop", &NO_PANIC);
1257 component.start().unwrap();
1258 component.hooks_fail = true;
1259
1260 let error = component.stop().unwrap_err();
1261
1262 assert_eq!(error.to_string(), "on_stop failed");
1263 assert_eq!(component.state(), ComponentState::Stopping);
1264 }
1265
1266 #[rstest]
1267 fn test_resume_halts_transition_when_on_resume_fails() {
1268 let mut component = TestComponent::new("failing-resume", &NO_PANIC);
1269 component.start().unwrap();
1270 component.stop().unwrap();
1271 component.hooks_fail = true;
1272
1273 let error = component.resume().unwrap_err();
1274
1275 assert_eq!(error.to_string(), "on_resume failed");
1276 assert_eq!(component.state(), ComponentState::Resuming);
1277 }
1278
1279 #[rstest]
1280 fn test_reset_retains_subscriptions_when_on_reset_fails() {
1281 let mut component = TestComponent::new("failing-reset", &NO_PANIC);
1282 component.hooks_fail = true;
1283
1284 let error = component.reset().unwrap_err();
1285
1286 assert_eq!(error.to_string(), "on_reset failed");
1287 assert_eq!(component.state(), ComponentState::Resetting);
1288 assert_eq!(component.releases, 0);
1289 }
1290
1291 #[rstest]
1292 fn test_reset_releases_subscriptions_on_success() {
1293 let mut component = TestComponent::new("resetting", &NO_PANIC);
1294
1295 component.reset().unwrap();
1296
1297 assert_eq!(component.state(), ComponentState::Ready);
1298 assert_eq!(component.releases, 1);
1299 }
1300
1301 #[rstest]
1302 fn test_dispose_faults_and_retains_subscriptions_when_on_dispose_fails() {
1303 let mut component = TestComponent::new("failing-dispose", &NO_PANIC);
1304 component.hooks_fail = true;
1305
1306 let error = component.dispose().unwrap_err();
1307
1308 assert_eq!(error.to_string(), "on_dispose failed");
1309 assert_eq!(component.state(), ComponentState::Faulted);
1310 assert_eq!(component.releases, 0);
1311 }
1312
1313 #[rstest]
1314 fn test_dispose_releases_subscriptions_on_success() {
1315 let mut component = TestComponent::new("disposing", &NO_PANIC);
1316
1317 component.dispose().unwrap();
1318
1319 assert_eq!(component.state(), ComponentState::Disposed);
1320 assert_eq!(component.releases, 1);
1321 }
1322
1323 #[rstest]
1324 fn test_fault_releases_subscriptions_even_when_on_fault_fails() {
1325 let mut component = TestComponent::new("failing-fault", &NO_PANIC);
1326 component.start().unwrap();
1327 component.hooks_fail = true;
1328
1329 let error = component.fault().unwrap_err();
1330
1331 assert_eq!(error.to_string(), "on_fault failed");
1332 assert_eq!(component.state(), ComponentState::Faulting);
1333 assert_eq!(component.releases, 1);
1334 }
1335
1336 #[rstest]
1337 fn test_fault_releases_subscriptions_on_success() {
1338 let mut component = TestComponent::new("faulting", &NO_PANIC);
1339 component.start().unwrap();
1340
1341 component.fault().unwrap();
1342
1343 assert_eq!(component.state(), ComponentState::Faulted);
1344 assert_eq!(component.releases, 1);
1345 }
1346
1347 #[rstest]
1348 fn test_registry_entry_points_reject_unknown_component() {
1349 clear_component_registry();
1350
1351 let id = Ustr::from("absent-component");
1352
1353 for (name, entry_point) in registry_entry_points() {
1354 let error = entry_point(&id).unwrap_err();
1355 assert_eq!(
1356 error.to_string(),
1357 "Component 'absent-component' not found in global registry",
1358 "unexpected error from {name}"
1359 );
1360 }
1361
1362 let error = component_state(&id).unwrap_err();
1363 assert_eq!(
1364 error.to_string(),
1365 "Component 'absent-component' not found in global registry"
1366 );
1367 }
1368
1369 #[rstest]
1370 fn test_registry_entry_points_reject_borrowed_component() {
1371 clear_component_registry();
1372
1373 let id = TestComponent::new("borrowed-component", &NO_PANIC).register_in_global_registry();
1374 assert!(with_component_registry(|registry| registry.try_borrow(id)));
1375
1376 for (name, entry_point) in registry_entry_points() {
1377 let error = entry_point(&id).unwrap_err();
1378 assert!(
1379 error
1380 .to_string()
1381 .starts_with("Component 'borrowed-component' is already mutably borrowed."),
1382 "unexpected error from {name}: {error}"
1383 );
1384 }
1385
1386 let error = component_state(&id).unwrap_err();
1387 assert!(
1388 error
1389 .to_string()
1390 .starts_with("Component 'borrowed-component' is already mutably borrowed.")
1391 );
1392
1393 assert!(with_component_registry(|registry| registry.is_borrowed(&id)));
1395 }
1396
1397 type RegistryEntryPoint = (&'static str, fn(&Ustr) -> anyhow::Result<()>);
1398
1399 fn registry_entry_points() -> [RegistryEntryPoint; 5] {
1401 [
1402 ("start", start_component),
1403 ("stop", stop_component),
1404 ("reset", reset_component),
1405 ("dispose", dispose_component),
1406 ("release", release_component_subscriptions),
1407 ]
1408 }
1409
1410 #[rstest]
1411 fn test_registry_entry_points_drive_the_full_lifecycle() {
1412 clear_component_registry();
1413
1414 let id = TestComponent::new("lifecycle-component", &NO_PANIC).register_in_global_registry();
1415
1416 assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1417
1418 start_component(&id).unwrap();
1419 assert_eq!(component_state(&id).unwrap(), ComponentState::Running);
1420
1421 stop_component(&id).unwrap();
1422 assert_eq!(component_state(&id).unwrap(), ComponentState::Stopped);
1423
1424 reset_component(&id).unwrap();
1425 assert_eq!(component_state(&id).unwrap(), ComponentState::Ready);
1426
1427 dispose_component(&id).unwrap();
1428 assert_eq!(component_state(&id).unwrap(), ComponentState::Disposed);
1429
1430 assert!(!with_component_registry(
1431 |registry| registry.is_borrowed(&id)
1432 ));
1433 }
1434
1435 #[rstest]
1436 fn test_release_component_subscriptions_invokes_the_component_hook() {
1437 clear_component_registry();
1438
1439 let component = TestComponent::new("releasing-component", &NO_PANIC);
1440 let id = component.id.inner();
1441 let component_ref = Rc::new(UnsafeCell::new(component));
1442 let observed = component_ref.clone();
1443 with_component_registry(|registry| {
1444 registry.insert(id, component_ref as Rc<UnsafeCell<dyn Component>>);
1445 });
1446
1447 release_component_subscriptions(&id).unwrap();
1448 release_component_subscriptions(&id).unwrap();
1449
1450 assert_eq!(unsafe { (*observed.get()).releases }, 2);
1452 }
1453
1454 #[rstest]
1455 fn test_deregister_component_removes_only_the_named_component() {
1456 clear_component_registry();
1457
1458 let kept = TestComponent::new("kept-component", &NO_PANIC).register_in_global_registry();
1459 let removed =
1460 TestComponent::new("removed-component", &NO_PANIC).register_in_global_registry();
1461
1462 deregister_component(&removed);
1463
1464 assert!(get_component(&removed).is_none());
1465 assert!(get_component(&kept).is_some());
1466 assert_eq!(component_state(&kept).unwrap(), ComponentState::Ready);
1467 }
1468
1469 #[rstest]
1470 fn test_component_registry_debug_reports_components_and_borrows() {
1471 clear_component_registry();
1472
1473 let id = TestComponent::new("debug-component", &NO_PANIC).register_in_global_registry();
1474 assert!(with_component_registry(|registry| registry.try_borrow(id)));
1475
1476 let debug = with_component_registry(|registry| format!("{registry:?}"));
1477
1478 assert!(debug.contains("debug-component"), "{debug}");
1479 assert!(debug.contains("active_borrows: 1"), "{debug}");
1480 }
1481
1482 #[rstest]
1483 fn test_registry_remove_returns_the_registered_component() {
1484 clear_component_registry();
1485
1486 let id = TestComponent::new("removable", &NO_PANIC).register_in_global_registry();
1487
1488 assert!(with_component_registry(|registry| registry.remove(&id)).is_some());
1489 assert!(with_component_registry(|registry| registry.remove(&id)).is_none());
1490 }
1491}