1use std::collections::{HashMap, HashSet};
15use std::future::Future;
16use std::pin::Pin;
17use std::time::Duration;
18
19use crate::{
20 HealthCheckDescriptor, HealthCheckOptions, HealthCheckRegistry, HealthCheckScope,
21 HealthComponentReport, ShutdownPhaseReport, ShutdownPhaseStatus, ShutdownReport,
22 StartupCoordinator, StartupPhaseFailurePolicy, StartupReport, SystemHealthReport,
23};
24
25type RuntimeShutdownFuture = Pin<Box<dyn Future<Output = Result<(), String>> + Send>>;
26type RuntimeShutdownPhaseFn = dyn FnMut() -> RuntimeShutdownFuture + Send;
27
28pub trait RuntimeComponentBundle {
35 fn register(self, registry: &mut RuntimeComponentRegistry);
37}
38
39impl<F> RuntimeComponentBundle for F
40where
41 F: FnOnce(&mut RuntimeComponentRegistry),
42{
43 fn register(self, registry: &mut RuntimeComponentRegistry) {
44 self(registry);
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum RuntimeComponentKind {
51 Core,
53 Database,
55 Cache,
57 Storage,
59 Mail,
61 Tasks,
63 ExternalAuth,
65 Product,
67}
68
69impl RuntimeComponentKind {
70 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::Core => "core",
74 Self::Database => "database",
75 Self::Cache => "cache",
76 Self::Storage => "storage",
77 Self::Mail => "mail",
78 Self::Tasks => "tasks",
79 Self::ExternalAuth => "external_auth",
80 Self::Product => "product",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct RuntimeShutdownDescriptor {
88 pub phase_name: &'static str,
90 pub timeout: Option<Duration>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct RuntimeStartupDescriptor {
97 pub phase_name: &'static str,
99 pub failure_policy: StartupPhaseFailurePolicy,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct RuntimeTaskDescriptor {
106 pub task_name: &'static str,
108 pub display_name: &'static str,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct RuntimeComponentDescriptor {
115 pub name: &'static str,
117 pub kind: RuntimeComponentKind,
119 pub dependencies: Vec<&'static str>,
121 pub startup: Vec<RuntimeStartupDescriptor>,
123 pub health_checks: Vec<HealthCheckDescriptor>,
125 pub tasks: Vec<RuntimeTaskDescriptor>,
127 pub shutdown: Vec<RuntimeShutdownDescriptor>,
129}
130
131impl RuntimeComponentDescriptor {
132 fn new(name: &'static str) -> Self {
133 Self {
134 name,
135 kind: RuntimeComponentKind::Product,
136 dependencies: Vec::new(),
137 startup: Vec::new(),
138 health_checks: Vec::new(),
139 tasks: Vec::new(),
140 shutdown: Vec::new(),
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
147pub enum RuntimeComponentGraphError {
148 #[error("runtime component '{component}' depends on missing component '{dependency}'")]
150 MissingDependency {
151 component: &'static str,
153 dependency: &'static str,
155 },
156 #[error("runtime component dependency cycle detected: {cycle}")]
158 Cycle {
159 cycle: String,
161 },
162}
163
164struct RuntimeComponentShutdownPhase {
165 component_name: &'static str,
166 phase_name: &'static str,
167 timeout: Option<Duration>,
168 phase: Box<RuntimeShutdownPhaseFn>,
169}
170
171#[derive(Default)]
173pub struct RuntimeComponentRegistry {
174 components: Vec<RuntimeComponentDescriptor>,
175 startup: StartupCoordinator,
176 health: HealthCheckRegistry,
177 shutdown: Vec<RuntimeComponentShutdownPhase>,
178}
179
180impl RuntimeComponentRegistry {
181 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn configured<F>(configure: F) -> Self
188 where
189 F: FnOnce(&mut Self),
190 {
191 let mut registry = Self::new();
192 registry.configure(configure);
193 registry
194 }
195
196 pub fn configure<F>(&mut self, configure: F) -> &mut Self
198 where
199 F: FnOnce(&mut Self),
200 {
201 configure(self);
202 self
203 }
204
205 pub fn register_bundle<B>(&mut self, bundle: B) -> &mut Self
207 where
208 B: RuntimeComponentBundle,
209 {
210 bundle.register(self);
211 self
212 }
213
214 pub fn component_health_with_options<F, Fut>(
216 &mut self,
217 component_name: &'static str,
218 kind: RuntimeComponentKind,
219 check_name: &'static str,
220 options: HealthCheckOptions,
221 check: F,
222 ) -> &mut Self
223 where
224 F: Fn() -> Fut + Send + Sync + 'static,
225 Fut: Future<Output = HealthComponentReport> + Send + 'static,
226 {
227 self.component(component_name)
228 .kind(kind)
229 .health_with_options(check_name, options, check);
230 self
231 }
232
233 pub fn component_startup<F, Fut>(
235 &mut self,
236 component_name: &'static str,
237 kind: RuntimeComponentKind,
238 phase_name: &'static str,
239 failure_policy: StartupPhaseFailurePolicy,
240 phase: F,
241 ) -> &mut Self
242 where
243 F: FnMut() -> Fut + Send + 'static,
244 Fut: Future<Output = Result<(), String>> + Send + 'static,
245 {
246 self.component(component_name)
247 .kind(kind)
248 .startup(phase_name, failure_policy, phase);
249 self
250 }
251
252 pub fn component_task(
254 &mut self,
255 component_name: &'static str,
256 kind: RuntimeComponentKind,
257 task_name: &'static str,
258 display_name: &'static str,
259 ) -> &mut Self {
260 self.component(component_name)
261 .kind(kind)
262 .task(task_name, display_name);
263 self
264 }
265
266 pub fn component_shutdown<F, Fut>(
268 &mut self,
269 component_name: &'static str,
270 kind: RuntimeComponentKind,
271 phase_name: &'static str,
272 timeout: Option<Duration>,
273 phase: F,
274 ) -> &mut Self
275 where
276 F: FnMut() -> Fut + Send + 'static,
277 Fut: Future<Output = Result<(), String>> + Send + 'static,
278 {
279 self.component(component_name)
280 .kind(kind)
281 .shutdown(phase_name, timeout, phase);
282 self
283 }
284
285 pub fn component_shutdown_once<T, F, Fut>(
287 &mut self,
288 component_name: &'static str,
289 kind: RuntimeComponentKind,
290 phase_name: &'static str,
291 timeout: Option<Duration>,
292 value: T,
293 phase: F,
294 ) -> &mut Self
295 where
296 T: Send + 'static,
297 F: FnOnce(T) -> Fut + Send + 'static,
298 Fut: Future<Output = Result<(), String>> + Send + 'static,
299 {
300 self.component(component_name)
301 .kind(kind)
302 .shutdown_once(phase_name, timeout, value, phase);
303 self
304 }
305
306 pub fn component(&mut self, name: &'static str) -> RuntimeComponentBuilder<'_> {
308 let index = match self
309 .components
310 .iter()
311 .position(|component| component.name == name)
312 {
313 Some(index) => index,
314 None => {
315 self.components.push(RuntimeComponentDescriptor::new(name));
316 self.components.len() - 1
317 }
318 };
319
320 RuntimeComponentBuilder {
321 registry: self,
322 index,
323 }
324 }
325
326 pub fn descriptors(&self) -> &[RuntimeComponentDescriptor] {
328 &self.components
329 }
330
331 pub fn descriptor(&self, name: &str) -> Option<&RuntimeComponentDescriptor> {
333 self.components
334 .iter()
335 .find(|component| component.name == name)
336 }
337
338 pub const fn health_registry(&self) -> &HealthCheckRegistry {
340 &self.health
341 }
342
343 pub const fn health_registry_mut(&mut self) -> &mut HealthCheckRegistry {
345 &mut self.health
346 }
347
348 pub async fn run_health(&mut self, scope: HealthCheckScope) -> SystemHealthReport {
350 self.health.run_scope(scope).await
351 }
352
353 pub async fn startup(&mut self) -> StartupReport {
355 self.startup.run().await
356 }
357
358 pub fn validate(&self) -> Result<(), RuntimeComponentGraphError> {
360 let descriptor_by_name = self
361 .components
362 .iter()
363 .map(|component| (component.name, component))
364 .collect::<HashMap<_, _>>();
365
366 for component in &self.components {
367 for dependency in &component.dependencies {
368 if !descriptor_by_name.contains_key(dependency) {
369 return Err(RuntimeComponentGraphError::MissingDependency {
370 component: component.name,
371 dependency,
372 });
373 }
374 }
375 }
376
377 let mut visiting = Vec::new();
378 let mut visited = HashSet::new();
379 for component in &self.components {
380 self.validate_component_dependencies(
381 component.name,
382 &descriptor_by_name,
383 &mut visiting,
384 &mut visited,
385 )?;
386 }
387
388 Ok(())
389 }
390
391 fn validate_component_dependencies(
392 &self,
393 component_name: &'static str,
394 descriptor_by_name: &HashMap<&'static str, &RuntimeComponentDescriptor>,
395 visiting: &mut Vec<&'static str>,
396 visited: &mut HashSet<&'static str>,
397 ) -> Result<(), RuntimeComponentGraphError> {
398 if visited.contains(component_name) {
399 return Ok(());
400 }
401 if let Some(position) = visiting
402 .iter()
403 .position(|visiting_name| *visiting_name == component_name)
404 {
405 let mut cycle = visiting[position..].to_vec();
406 cycle.push(component_name);
407 return Err(RuntimeComponentGraphError::Cycle {
408 cycle: cycle.join(" -> "),
409 });
410 }
411
412 visiting.push(component_name);
413 if let Some(descriptor) = descriptor_by_name.get(component_name) {
414 for dependency in &descriptor.dependencies {
415 self.validate_component_dependencies(
416 dependency,
417 descriptor_by_name,
418 visiting,
419 visited,
420 )?;
421 }
422 }
423 visiting.pop();
424 visited.insert(component_name);
425 Ok(())
426 }
427
428 pub async fn shutdown(&mut self) -> ShutdownReport {
437 let mut reports = Vec::with_capacity(self.shutdown.len());
438 for index in self.shutdown_order() {
439 let registered = &mut self.shutdown[index];
440 tracing::info!(phase = registered.phase_name, "starting shutdown phase");
441 let started_at = std::time::Instant::now();
442 let future = (registered.phase)();
443 let status = match registered.timeout {
444 Some(timeout) => match tokio::time::timeout(timeout, future).await {
445 Ok(Ok(())) => ShutdownPhaseStatus::Succeeded,
446 Ok(Err(error)) => ShutdownPhaseStatus::Failed(error),
447 Err(_) => ShutdownPhaseStatus::TimedOut,
448 },
449 None => match future.await {
450 Ok(()) => ShutdownPhaseStatus::Succeeded,
451 Err(error) => ShutdownPhaseStatus::Failed(error),
452 },
453 };
454 let duration = started_at.elapsed();
455 match &status {
456 ShutdownPhaseStatus::Succeeded => {
457 tracing::info!(
458 phase = registered.phase_name,
459 ?duration,
460 "shutdown phase completed"
461 );
462 }
463 ShutdownPhaseStatus::Failed(error) => {
464 tracing::error!(
465 phase = registered.phase_name,
466 ?duration,
467 %error,
468 "shutdown phase failed"
469 );
470 }
471 ShutdownPhaseStatus::TimedOut => {
472 tracing::error!(
473 phase = registered.phase_name,
474 ?duration,
475 "shutdown phase timed out"
476 );
477 }
478 }
479 reports.push(ShutdownPhaseReport {
480 name: registered.phase_name,
481 status,
482 duration,
483 });
484 }
485
486 ShutdownReport::new(reports)
487 }
488
489 fn shutdown_order(&self) -> Vec<usize> {
490 let mut phase_indices_by_component: HashMap<&'static str, Vec<usize>> = HashMap::new();
491 for (index, phase) in self.shutdown.iter().enumerate() {
492 phase_indices_by_component
493 .entry(phase.component_name)
494 .or_default()
495 .push(index);
496 }
497 let descriptor_by_name = self
498 .components
499 .iter()
500 .map(|component| (component.name, component))
501 .collect::<HashMap<_, _>>();
502 let mut visiting = HashSet::new();
503 let mut visited = HashSet::new();
504 let mut ordered = Vec::with_capacity(self.shutdown.len());
505
506 for phase in &self.shutdown {
507 self.push_shutdown_component_order(
508 phase.component_name,
509 &phase_indices_by_component,
510 &descriptor_by_name,
511 &mut visiting,
512 &mut visited,
513 &mut ordered,
514 );
515 }
516
517 ordered
518 }
519
520 fn push_shutdown_component_order(
521 &self,
522 component_name: &'static str,
523 phase_indices_by_component: &HashMap<&'static str, Vec<usize>>,
524 descriptor_by_name: &HashMap<&'static str, &RuntimeComponentDescriptor>,
525 visiting: &mut HashSet<&'static str>,
526 visited: &mut HashSet<&'static str>,
527 ordered: &mut Vec<usize>,
528 ) {
529 if visited.contains(component_name) {
530 return;
531 }
532 if !visiting.insert(component_name) {
533 tracing::warn!(
534 component = component_name,
535 "runtime component dependency cycle detected during shutdown ordering"
536 );
537 return;
538 }
539
540 if let Some(descriptor) = descriptor_by_name.get(component_name) {
541 for dependency in &descriptor.dependencies {
542 if phase_indices_by_component.contains_key(dependency) {
543 self.push_shutdown_component_order(
544 dependency,
545 phase_indices_by_component,
546 descriptor_by_name,
547 visiting,
548 visited,
549 ordered,
550 );
551 }
552 }
553 }
554
555 visiting.remove(component_name);
556 visited.insert(component_name);
557 if let Some(indices) = phase_indices_by_component.get(component_name) {
560 ordered.extend(indices);
561 }
562 }
563
564 pub fn len(&self) -> usize {
566 self.components.len()
567 }
568
569 pub fn is_empty(&self) -> bool {
571 self.components.is_empty()
572 }
573}
574
575pub struct RuntimeComponentBuilder<'a> {
577 registry: &'a mut RuntimeComponentRegistry,
578 index: usize,
579}
580
581impl RuntimeComponentBuilder<'_> {
582 pub fn kind(&mut self, kind: RuntimeComponentKind) -> &mut Self {
584 self.descriptor_mut().kind = kind;
585 self
586 }
587
588 pub fn depends_on(&mut self, dependency: &'static str) -> &mut Self {
590 let descriptor = self.descriptor_mut();
591 if !descriptor.dependencies.contains(&dependency) {
592 descriptor.dependencies.push(dependency);
593 }
594 self
595 }
596
597 pub fn depends_on_all(&mut self, dependencies: &[&'static str]) -> &mut Self {
599 for dependency in dependencies {
600 self.depends_on(dependency);
601 }
602 self
603 }
604
605 pub fn startup<F, Fut>(
607 &mut self,
608 phase_name: &'static str,
609 failure_policy: StartupPhaseFailurePolicy,
610 phase: F,
611 ) -> &mut Self
612 where
613 F: FnMut() -> Fut + Send + 'static,
614 Fut: Future<Output = Result<(), String>> + Send + 'static,
615 {
616 self.registry
617 .startup
618 .phase(phase_name, failure_policy, phase);
619 self.descriptor_mut()
620 .startup
621 .push(RuntimeStartupDescriptor {
622 phase_name,
623 failure_policy,
624 });
625 self
626 }
627
628 pub fn task(&mut self, task_name: &'static str, display_name: &'static str) -> &mut Self {
630 self.descriptor_mut().tasks.push(RuntimeTaskDescriptor {
631 task_name,
632 display_name,
633 });
634 self
635 }
636
637 pub fn health_with_options<F, Fut>(
639 &mut self,
640 check_name: &'static str,
641 options: HealthCheckOptions,
642 check: F,
643 ) -> &mut Self
644 where
645 F: Fn() -> Fut + Send + Sync + 'static,
646 Fut: Future<Output = HealthComponentReport> + Send + 'static,
647 {
648 self.registry
649 .health
650 .register_with_options(check_name, options, check);
651 self.descriptor_mut()
652 .health_checks
653 .push(HealthCheckDescriptor {
654 name: check_name,
655 requirement: options.requirement,
656 timeout: options.timeout,
657 scopes: options.scopes,
658 });
659 self
660 }
661
662 pub fn shutdown<F, Fut>(
670 &mut self,
671 phase_name: &'static str,
672 timeout: Option<Duration>,
673 mut phase: F,
674 ) -> &mut Self
675 where
676 F: FnMut() -> Fut + Send + 'static,
677 Fut: Future<Output = Result<(), String>> + Send + 'static,
678 {
679 let component_name = self.descriptor_mut().name;
680 if self.registry.shutdown.iter().any(|registered| {
681 registered.component_name == component_name && registered.phase_name == phase_name
682 }) {
683 tracing::warn!(
684 component = component_name,
685 phase = phase_name,
686 "duplicate shutdown phase registration"
687 );
688 }
689 self.registry.shutdown.push(RuntimeComponentShutdownPhase {
690 component_name,
691 phase_name,
692 timeout,
693 phase: Box::new(move || Box::pin(phase())),
694 });
695 self.descriptor_mut()
696 .shutdown
697 .push(RuntimeShutdownDescriptor {
698 phase_name,
699 timeout,
700 });
701 self
702 }
703
704 pub fn shutdown_once<T, F, Fut>(
710 &mut self,
711 phase_name: &'static str,
712 timeout: Option<Duration>,
713 value: T,
714 phase: F,
715 ) -> &mut Self
716 where
717 T: Send + 'static,
718 F: FnOnce(T) -> Fut + Send + 'static,
719 Fut: Future<Output = Result<(), String>> + Send + 'static,
720 {
721 let mut value = Some(value);
722 let mut phase = Some(phase);
723 self.shutdown(phase_name, timeout, move || {
724 let value = value.take();
725 let phase = phase.take();
726 async move {
727 if let (Some(value), Some(phase)) = (value, phase) {
728 phase(value).await?;
729 }
730 Ok(())
731 }
732 })
733 }
734
735 fn descriptor_mut(&mut self) -> &mut RuntimeComponentDescriptor {
736 &mut self.registry.components[self.index]
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use std::sync::{Arc, Mutex};
743 use std::time::Duration;
744
745 use super::{RuntimeComponentBundle, RuntimeComponentKind, RuntimeComponentRegistry};
746 use crate::{
747 HealthCheckOptions, HealthCheckScope, HealthCheckScopes, HealthComponentReport,
748 HealthStatus, ShutdownPhaseStatus, StartupPhaseFailurePolicy,
749 };
750
751 #[tokio::test]
752 async fn registry_runs_component_health_checks_by_scope() {
753 let mut registry = RuntimeComponentRegistry::new();
754 registry.component_health_with_options(
755 "database",
756 RuntimeComponentKind::Database,
757 "database",
758 HealthCheckOptions::required(Some(Duration::from_secs(1)))
759 .with_scopes(HealthCheckScopes::readiness_and_diagnostics()),
760 || async { HealthComponentReport::healthy("database", "ok") },
761 );
762 registry.component_health_with_options(
763 "cache",
764 RuntimeComponentKind::Cache,
765 "cache",
766 HealthCheckOptions::optional(None).with_scopes(HealthCheckScopes::diagnostics()),
767 || async { HealthComponentReport::degraded("cache", "fallback") },
768 );
769
770 let readiness = registry.run_health(HealthCheckScope::Readiness).await;
771 let diagnostics = registry.run_health(HealthCheckScope::Diagnostics).await;
772
773 assert_eq!(readiness.components.len(), 1);
774 assert_eq!(readiness.components[0].name, "database");
775 assert_eq!(diagnostics.components.len(), 2);
776 assert_eq!(diagnostics.status(), HealthStatus::Degraded);
777 assert_eq!(registry.descriptors()[0].health_checks[0].name, "database");
778 assert_eq!(registry.descriptors()[1].health_checks[0].name, "cache");
779 }
780
781 #[tokio::test]
782 async fn registry_runs_component_startup_and_records_task_descriptors() {
783 let startup_events = Arc::new(Mutex::new(Vec::new()));
784 let mut registry = RuntimeComponentRegistry::new();
785 registry
786 .component("mail")
787 .kind(RuntimeComponentKind::Mail)
788 .startup("mail_templates", StartupPhaseFailurePolicy::Required, {
789 let startup_events = Arc::clone(&startup_events);
790 move || {
791 let startup_events = Arc::clone(&startup_events);
792 async move {
793 startup_events.lock().unwrap().push("mail_templates");
794 Ok(())
795 }
796 }
797 })
798 .task("mail-outbox-dispatch", "Mail outbox dispatch");
799
800 let report = registry.startup().await;
801
802 assert!(!report.aborted());
803 assert_eq!(
804 startup_events.lock().unwrap().as_slice(),
805 ["mail_templates"]
806 );
807 let descriptor = registry.descriptor("mail").expect("mail descriptor");
808 assert_eq!(descriptor.startup[0].phase_name, "mail_templates");
809 assert_eq!(
810 descriptor.startup[0].failure_policy,
811 StartupPhaseFailurePolicy::Required
812 );
813 assert_eq!(descriptor.tasks[0].task_name, "mail-outbox-dispatch");
814 assert_eq!(descriptor.tasks[0].display_name, "Mail outbox dispatch");
815 }
816
817 fn register_database_component(registry: &mut RuntimeComponentRegistry) {
818 registry.component_health_with_options(
819 "database",
820 RuntimeComponentKind::Database,
821 "database",
822 HealthCheckOptions::required(None),
823 || async { HealthComponentReport::healthy("database", "ok") },
824 );
825 }
826
827 fn register_cache_component(registry: &mut RuntimeComponentRegistry) {
828 registry
829 .component("cache")
830 .kind(RuntimeComponentKind::Cache)
831 .depends_on("database")
832 .health_with_options(
833 "cache",
834 HealthCheckOptions::optional(None).with_scopes(HealthCheckScopes::diagnostics()),
835 || async { HealthComponentReport::healthy("cache", "ok") },
836 );
837 }
838
839 #[tokio::test]
840 async fn registry_runs_shutdown_phases_in_dependency_order() {
841 let order = Arc::new(Mutex::new(Vec::new()));
842 let mut registry = RuntimeComponentRegistry::new();
843
844 registry
845 .component("database")
846 .kind(RuntimeComponentKind::Database)
847 .depends_on_all(&["tasks"])
848 .shutdown("database", None, {
849 let order = Arc::clone(&order);
850 move || {
851 let order = Arc::clone(&order);
852 async move {
853 order.lock().unwrap().push("database");
854 Err("close failed".to_string())
855 }
856 }
857 });
858 registry.component_shutdown("tasks", RuntimeComponentKind::Tasks, "tasks", None, {
859 let order = Arc::clone(&order);
860 move || {
861 let order = Arc::clone(&order);
862 async move {
863 order.lock().unwrap().push("tasks");
864 Ok(())
865 }
866 }
867 });
868
869 let report = registry.shutdown().await;
870
871 assert_eq!(order.lock().unwrap().as_slice(), ["tasks", "database"]);
872 assert!(report.has_failures());
873 assert_eq!(report.phases[0].status, ShutdownPhaseStatus::Succeeded);
874 assert_eq!(
875 report.phases[1].status,
876 ShutdownPhaseStatus::Failed("close failed".to_string())
877 );
878 assert_eq!(
879 registry
880 .descriptor("database")
881 .expect("database component should exist")
882 .dependencies,
883 vec!["tasks"]
884 );
885 }
886
887 #[tokio::test]
888 async fn registry_runs_deep_shutdown_graph_before_dependents() {
889 let order = Arc::new(Mutex::new(Vec::new()));
890 let mut registry = RuntimeComponentRegistry::new();
891
892 for (component, kind, dependencies) in [
893 (
894 "database",
895 RuntimeComponentKind::Database,
896 &["audit_manager"][..],
897 ),
898 (
899 "audit_manager",
900 RuntimeComponentKind::Product,
901 &["audit_logs"][..],
902 ),
903 (
904 "audit_logs",
905 RuntimeComponentKind::Product,
906 &["mail_outbox"][..],
907 ),
908 (
909 "mail_outbox",
910 RuntimeComponentKind::Mail,
911 &["background_tasks"][..],
912 ),
913 ("background_tasks", RuntimeComponentKind::Tasks, &[][..]),
914 ] {
915 registry
916 .component(component)
917 .kind(kind)
918 .depends_on_all(dependencies)
919 .shutdown(component, None, {
920 let order = Arc::clone(&order);
921 move || {
922 let order = Arc::clone(&order);
923 async move {
924 order.lock().unwrap().push(component);
925 Ok(())
926 }
927 }
928 });
929 }
930
931 let report = registry.shutdown().await;
932
933 assert!(!report.has_failures());
934 assert_eq!(
935 order.lock().unwrap().as_slice(),
936 [
937 "background_tasks",
938 "mail_outbox",
939 "audit_logs",
940 "audit_manager",
941 "database"
942 ]
943 );
944 assert_eq!(
945 report
946 .phases
947 .iter()
948 .map(|phase| phase.name)
949 .collect::<Vec<_>>(),
950 vec![
951 "background_tasks",
952 "mail_outbox",
953 "audit_logs",
954 "audit_manager",
955 "database"
956 ]
957 );
958 }
959
960 #[tokio::test]
961 async fn registry_runs_all_shutdown_phases_of_one_component_in_registration_order() {
962 let order = Arc::new(Mutex::new(Vec::new()));
963 let mut registry = RuntimeComponentRegistry::new();
964
965 {
966 let mut component = registry.component("audit");
967 for phase_name in ["audit_stop", "audit_flush", "audit_close"] {
968 component.shutdown(phase_name, None, {
969 let order = Arc::clone(&order);
970 move || {
971 let order = Arc::clone(&order);
972 async move {
973 order.lock().unwrap().push(phase_name);
974 Ok(())
975 }
976 }
977 });
978 }
979 }
980
981 let report = registry.shutdown().await;
982
983 assert!(!report.has_failures());
984 assert_eq!(
985 order.lock().unwrap().as_slice(),
986 ["audit_stop", "audit_flush", "audit_close"]
987 );
988 assert_eq!(
989 report
990 .phases
991 .iter()
992 .map(|phase| phase.name)
993 .collect::<Vec<_>>(),
994 vec!["audit_stop", "audit_flush", "audit_close"]
995 );
996 assert_eq!(
997 registry
998 .descriptor("audit")
999 .expect("audit component should exist")
1000 .shutdown
1001 .iter()
1002 .map(|descriptor| descriptor.phase_name)
1003 .collect::<Vec<_>>(),
1004 vec!["audit_stop", "audit_flush", "audit_close"]
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn registry_runs_dependency_phases_before_all_dependent_phases() {
1010 let order = Arc::new(Mutex::new(Vec::new()));
1011 let mut registry = RuntimeComponentRegistry::new();
1012
1013 {
1014 let mut database = registry.component("database");
1015 for phase_name in ["database_stop", "database_close"] {
1016 database.shutdown(phase_name, None, {
1017 let order = Arc::clone(&order);
1018 move || {
1019 let order = Arc::clone(&order);
1020 async move {
1021 order.lock().unwrap().push(phase_name);
1022 Ok(())
1023 }
1024 }
1025 });
1026 }
1027 }
1028 {
1029 let mut audit = registry.component("audit");
1030 audit.depends_on("database");
1031 for phase_name in ["audit_flush", "audit_close"] {
1032 audit.shutdown(phase_name, None, {
1033 let order = Arc::clone(&order);
1034 move || {
1035 let order = Arc::clone(&order);
1036 async move {
1037 order.lock().unwrap().push(phase_name);
1038 Ok(())
1039 }
1040 }
1041 });
1042 }
1043 }
1044
1045 let report = registry.shutdown().await;
1046
1047 assert!(!report.has_failures());
1048 assert_eq!(
1049 order.lock().unwrap().as_slice(),
1050 [
1051 "database_stop",
1052 "database_close",
1053 "audit_flush",
1054 "audit_close"
1055 ]
1056 );
1057 }
1058
1059 #[derive(Clone)]
1060 struct SharedLogBuffer(Arc<Mutex<Vec<u8>>>);
1061
1062 impl std::io::Write for SharedLogBuffer {
1063 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1064 self.0.lock().unwrap().extend_from_slice(buf);
1065 Ok(buf.len())
1066 }
1067
1068 fn flush(&mut self) -> std::io::Result<()> {
1069 Ok(())
1070 }
1071 }
1072
1073 #[tokio::test]
1074 async fn duplicate_shutdown_phase_registration_warns_and_still_runs_both() {
1075 let order = Arc::new(Mutex::new(Vec::new()));
1076 let buffer = SharedLogBuffer(Arc::new(Mutex::new(Vec::new())));
1077 let writer = buffer.clone();
1078 let subscriber = tracing_subscriber::fmt()
1079 .with_writer(move || writer.clone())
1080 .with_ansi(false)
1081 .without_time()
1082 .finish();
1083
1084 let mut registry = RuntimeComponentRegistry::new();
1085 tracing::subscriber::with_default(subscriber, || {
1086 let mut component = registry.component("audit");
1087 for run in [1_u8, 2] {
1088 component.shutdown("audit_flush", None, {
1089 let order = Arc::clone(&order);
1090 move || {
1091 let order = Arc::clone(&order);
1092 async move {
1093 order.lock().unwrap().push(run);
1094 Ok(())
1095 }
1096 }
1097 });
1098 }
1099 });
1100
1101 let logs = String::from_utf8(buffer.0.lock().unwrap().clone())
1102 .expect("log output should be valid UTF-8");
1103 assert!(
1104 logs.contains("duplicate shutdown phase registration"),
1105 "expected duplicate registration warning, got: {logs}"
1106 );
1107
1108 let report = registry.shutdown().await;
1109 assert!(!report.has_failures());
1110 assert_eq!(order.lock().unwrap().as_slice(), [1, 2]);
1111 assert_eq!(report.phases.len(), 2);
1112 }
1113
1114 #[tokio::test]
1115 async fn registry_ignores_shutdown_dependencies_without_shutdown_phase() {
1116 let order = Arc::new(Mutex::new(Vec::new()));
1117 let mut registry = RuntimeComponentRegistry::new();
1118
1119 registry
1120 .component("cache")
1121 .kind(RuntimeComponentKind::Cache);
1122 registry
1123 .component("database")
1124 .kind(RuntimeComponentKind::Database)
1125 .depends_on("cache")
1126 .shutdown("database", None, {
1127 let order = Arc::clone(&order);
1128 move || {
1129 let order = Arc::clone(&order);
1130 async move {
1131 order.lock().unwrap().push("database");
1132 Ok(())
1133 }
1134 }
1135 });
1136
1137 let report = registry.shutdown().await;
1138
1139 assert!(!report.has_failures());
1140 assert_eq!(order.lock().unwrap().as_slice(), ["database"]);
1141 assert_eq!(report.phases.len(), 1);
1142 assert_eq!(report.phases[0].name, "database");
1143 }
1144
1145 #[test]
1146 fn registry_validate_rejects_missing_component_dependencies() {
1147 let mut registry = RuntimeComponentRegistry::new();
1148 registry
1149 .component("database")
1150 .kind(RuntimeComponentKind::Database)
1151 .depends_on("cache");
1152
1153 let error = registry
1154 .validate()
1155 .expect_err("missing dependency should fail validation");
1156
1157 assert_eq!(
1158 error,
1159 crate::RuntimeComponentGraphError::MissingDependency {
1160 component: "database",
1161 dependency: "cache"
1162 }
1163 );
1164 }
1165
1166 #[test]
1167 fn registry_validate_rejects_dependency_cycles() {
1168 let mut registry = RuntimeComponentRegistry::new();
1169 registry.component("database").depends_on("audit");
1170 registry.component("audit").depends_on("database");
1171
1172 let error = registry
1173 .validate()
1174 .expect_err("dependency cycle should fail validation");
1175
1176 assert_eq!(
1177 error,
1178 crate::RuntimeComponentGraphError::Cycle {
1179 cycle: "database -> audit -> database".to_string()
1180 }
1181 );
1182 }
1183
1184 #[tokio::test]
1185 async fn registry_shutdown_dependency_cycle_does_not_repeat_phases() {
1186 let order = Arc::new(Mutex::new(Vec::new()));
1187 let mut registry = RuntimeComponentRegistry::new();
1188
1189 for (component, dependency) in [("database", "audit"), ("audit", "database")] {
1190 registry
1191 .component(component)
1192 .kind(RuntimeComponentKind::Product)
1193 .depends_on(dependency)
1194 .shutdown(component, None, {
1195 let order = Arc::clone(&order);
1196 move || {
1197 let order = Arc::clone(&order);
1198 async move {
1199 order.lock().unwrap().push(component);
1200 Ok(())
1201 }
1202 }
1203 });
1204 }
1205
1206 let report = registry.shutdown().await;
1207 let order = order.lock().unwrap();
1208
1209 assert!(!report.has_failures());
1210 assert_eq!(order.len(), 2);
1211 assert!(order.contains(&"database"));
1212 assert!(order.contains(&"audit"));
1213 assert_eq!(report.phases.len(), 2);
1214 }
1215
1216 #[tokio::test]
1217 async fn registry_shutdown_once_consumes_owned_value_once() {
1218 let values = Arc::new(Mutex::new(Vec::new()));
1219 let mut registry = RuntimeComponentRegistry::new();
1220
1221 registry.component_shutdown_once(
1222 "database",
1223 RuntimeComponentKind::Database,
1224 "database",
1225 None,
1226 "writer",
1227 {
1228 let values = Arc::clone(&values);
1229 move |value| async move {
1230 values.lock().unwrap().push(value);
1231 Ok(())
1232 }
1233 },
1234 );
1235
1236 let first = registry.shutdown().await;
1237 let second = registry.shutdown().await;
1238
1239 assert!(!first.has_failures());
1240 assert!(!second.has_failures());
1241 assert_eq!(values.lock().unwrap().as_slice(), ["writer"]);
1242 assert_eq!(
1243 registry
1244 .descriptor("database")
1245 .map(|descriptor| descriptor.kind),
1246 Some(RuntimeComponentKind::Database)
1247 );
1248 }
1249
1250 struct TestShutdownBundle {
1251 values: Arc<Mutex<Vec<&'static str>>>,
1252 }
1253
1254 impl RuntimeComponentBundle for TestShutdownBundle {
1255 fn register(self, registry: &mut RuntimeComponentRegistry) {
1256 registry.component_shutdown("audit", RuntimeComponentKind::Product, "audit", None, {
1257 let values = Arc::clone(&self.values);
1258 move || {
1259 let values = Arc::clone(&values);
1260 async move {
1261 values.lock().unwrap().push("audit");
1262 Ok(())
1263 }
1264 }
1265 });
1266 registry
1267 .component("database")
1268 .kind(RuntimeComponentKind::Database)
1269 .depends_on("audit")
1270 .shutdown_once("database", None, self.values, |values| async move {
1271 values.lock().unwrap().push("database");
1272 Ok(())
1273 });
1274 }
1275 }
1276
1277 #[tokio::test]
1278 async fn registry_accepts_owned_component_bundle() {
1279 let values = Arc::new(Mutex::new(Vec::new()));
1280 let mut registry = RuntimeComponentRegistry::new();
1281 registry.register_bundle(TestShutdownBundle {
1282 values: Arc::clone(&values),
1283 });
1284
1285 assert_eq!(registry.len(), 2);
1286 assert_eq!(
1287 registry
1288 .descriptor("database")
1289 .expect("database descriptor should exist")
1290 .dependencies,
1291 vec!["audit"]
1292 );
1293
1294 let report = registry.shutdown().await;
1295
1296 assert!(!report.has_failures());
1297 assert_eq!(values.lock().unwrap().as_slice(), ["audit", "database"]);
1298 }
1299
1300 #[test]
1301 fn registry_accepts_closure_component_bundle() {
1302 let mut registry = RuntimeComponentRegistry::new();
1303 registry.register_bundle(|registry: &mut RuntimeComponentRegistry| {
1304 register_database_component(registry);
1305 });
1306
1307 assert_eq!(registry.len(), 1);
1308 assert_eq!(registry.descriptors()[0].name, "database");
1309 }
1310
1311 #[test]
1312 fn registry_register_bundle_chains_multiple_component_bundles() {
1313 let mut registry = RuntimeComponentRegistry::new();
1314 registry
1315 .register_bundle(register_database_component)
1316 .register_bundle(register_cache_component);
1317
1318 assert_eq!(registry.len(), 2);
1319 assert_eq!(registry.descriptors()[0].name, "database");
1320 assert_eq!(registry.descriptors()[1].name, "cache");
1321 }
1322
1323 #[tokio::test]
1324 async fn registry_can_shutdown_registered_component_bundle() {
1325 let values = Arc::new(Mutex::new(Vec::new()));
1326
1327 let mut registry = RuntimeComponentRegistry::new();
1328 registry.register_bundle(TestShutdownBundle {
1329 values: Arc::clone(&values),
1330 });
1331 let report = registry.shutdown().await;
1332
1333 assert!(!report.has_failures());
1334 assert_eq!(values.lock().unwrap().as_slice(), ["audit", "database"]);
1335 }
1336}