1use axum::Router;
17use std::collections::HashSet;
18use std::sync::Arc;
19
20use tokio_util::sync::CancellationToken;
21use uuid::Uuid;
22
23use crate::backends::OopSpawnConfig;
24use crate::client_hub::ClientHub;
25use crate::config::ConfigProvider;
26use crate::context::GearContextBuilder;
27use crate::registry::{
28 ApiGatewayCap, GearEntry, GearRegistry, GrpcHubCap, RegistryError, RestApiCap, RunnableCap,
29 SystemCap,
30};
31use crate::runtime::{GearManager, GrpcInstallerStore, OopSpawnOptions, SystemContext};
32
33#[cfg(feature = "db")]
34use crate::registry::DatabaseCap;
35
36#[derive(Clone)]
38pub enum DbOptions {
39 None,
41 #[cfg(feature = "db")]
43 Manager(Arc<toolkit_db::DbManager>),
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum RunMode {
49 Full,
51 MigrateOnly,
53}
54
55pub const TOOLKIT_DIRECTORY_ENDPOINT_ENV: &str = "TOOLKIT_DIRECTORY_ENDPOINT";
57
58pub const TOOLKIT_MODULE_CONFIG_ENV: &str = "TOOLKIT_MODULE_CONFIG";
60
61pub const DEFAULT_SHUTDOWN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(35);
67
68pub struct HostRuntime {
72 registry: GearRegistry,
73 ctx_builder: GearContextBuilder,
74 instance_id: Uuid,
75 gear_manager: Arc<GearManager>,
76 grpc_installers: Arc<GrpcInstallerStore>,
77 #[allow(dead_code)]
78 client_hub: Arc<ClientHub>,
79 cancel: CancellationToken,
80 #[allow(dead_code)]
81 db_options: DbOptions,
82 oop_options: Option<OopSpawnOptions>,
84 shutdown_deadline: std::time::Duration,
86}
87
88impl HostRuntime {
89 pub fn new(
93 registry: GearRegistry,
94 gears_cfg: Arc<dyn ConfigProvider>,
95 db_options: DbOptions,
96 client_hub: Arc<ClientHub>,
97 cancel: CancellationToken,
98 instance_id: Uuid,
99 oop_options: Option<OopSpawnOptions>,
100 ) -> Self {
101 let gear_manager = Arc::new(GearManager::new());
103 let grpc_installers = Arc::new(GrpcInstallerStore::new());
104
105 let ctx_builder =
107 GearContextBuilder::new(instance_id, gears_cfg, client_hub.clone(), cancel.clone());
108 #[cfg(feature = "db")]
109 let ctx_builder = match &db_options {
110 DbOptions::Manager(mgr) => ctx_builder.with_db_manager(mgr.clone()),
111 DbOptions::None => ctx_builder,
112 };
113
114 Self {
115 registry,
116 ctx_builder,
117 instance_id,
118 gear_manager,
119 grpc_installers,
120 client_hub,
121 cancel,
122 db_options,
123 oop_options,
124 shutdown_deadline: DEFAULT_SHUTDOWN_DEADLINE,
125 }
126 }
127
128 #[must_use]
144 pub fn with_shutdown_deadline(mut self, deadline: std::time::Duration) -> Self {
145 self.shutdown_deadline = deadline;
146 self
147 }
148
149 pub fn run_pre_init_phase(&self) -> Result<(), RegistryError> {
156 tracing::info!("Phase: pre_init");
157
158 let sys_ctx = SystemContext::new(
159 self.instance_id,
160 Arc::clone(&self.gear_manager),
161 Arc::clone(&self.grpc_installers),
162 );
163
164 for entry in self.registry.gears() {
165 if self.cancel.is_cancelled() {
167 tracing::warn!("Pre-init phase cancelled by signal");
168 return Err(RegistryError::Cancelled);
169 }
170
171 if let Some(sys_mod) = entry.caps.query::<SystemCap>() {
172 tracing::debug!(gear = entry.name, "Running system pre_init");
173 sys_mod
174 .pre_init(&sys_ctx)
175 .map_err(|e| RegistryError::PreInit {
176 gear: entry.name,
177 source: e,
178 })?;
179 }
180 }
181
182 Ok(())
183 }
184
185 #[cfg(feature = "db")]
187 async fn gear_context(
188 &self,
189 gear_name: &'static str,
190 ) -> Result<crate::context::GearCtx, RegistryError> {
191 self.ctx_builder
192 .for_gear(gear_name)
193 .await
194 .map_err(|e| RegistryError::DbMigrate {
195 gear: gear_name,
196 source: e,
197 })
198 }
199
200 #[cfg(feature = "db")]
202 async fn db_migration_target(
203 &self,
204 gear_name: &'static str,
205 ctx: &crate::context::GearCtx,
206 db_gear: Option<Arc<dyn crate::contracts::DatabaseCapability>>,
207 ) -> Result<
208 Option<(
209 toolkit_db::Db,
210 Arc<dyn crate::contracts::DatabaseCapability>,
211 )>,
212 RegistryError,
213 > {
214 let Some(dbm) = db_gear else {
215 return Ok(None);
216 };
217
218 let db = match &self.db_options {
222 DbOptions::None => None,
223 #[cfg(feature = "db")]
224 DbOptions::Manager(mgr) => {
225 mgr.get(gear_name)
226 .await
227 .map_err(|e| RegistryError::DbMigrate {
228 gear: gear_name,
229 source: e.into(),
230 })?
231 }
232 };
233
234 _ = ctx; Ok(db.map(|db| (db, dbm)))
236 }
237
238 #[cfg(feature = "db")]
243 async fn migrate_gear(
244 gear_name: &'static str,
245 db: &toolkit_db::Db,
246 db_gear: Arc<dyn crate::contracts::DatabaseCapability>,
247 ) -> Result<(), RegistryError> {
248 let migrations = db_gear.migrations();
250
251 if migrations.is_empty() {
252 tracing::debug!(gear = gear_name, "No migrations to run");
253 return Ok(());
254 }
255
256 tracing::debug!(
257 gear = gear_name,
258 count = migrations.len(),
259 "Running DB migrations"
260 );
261
262 let result =
264 toolkit_db::migration_runner::run_migrations_for_gear(db, gear_name, migrations)
265 .await
266 .map_err(|e| RegistryError::DbMigrate {
267 gear: gear_name,
268 source: anyhow::Error::new(e),
269 })?;
270
271 tracing::info!(
272 gear = gear_name,
273 applied = result.applied,
274 skipped = result.skipped,
275 "DB migrations completed"
276 );
277
278 Ok(())
279 }
280
281 #[cfg(feature = "db")]
290 async fn run_db_phase(&self) -> Result<(), RegistryError> {
291 tracing::info!("Phase: db (before init)");
292
293 for entry in self.registry.gears_by_system_priority() {
294 if self.cancel.is_cancelled() {
296 tracing::warn!("DB migration phase cancelled by signal");
297 return Err(RegistryError::Cancelled);
298 }
299
300 let ctx = self.gear_context(entry.name).await?;
301 let db_gear = entry.caps.query::<DatabaseCap>();
302
303 match self
304 .db_migration_target(entry.name, &ctx, db_gear.clone())
305 .await?
306 {
307 Some((db, dbm)) => {
308 Self::migrate_gear(entry.name, &db, dbm).await?;
309 }
310 None if db_gear.is_some() => {
311 tracing::debug!(
312 gear = entry.name,
313 "Gear has DbGear trait but no DB handle (no config)"
314 );
315 }
316 None => {}
317 }
318 }
319
320 Ok(())
321 }
322
323 async fn run_init_phase(&self) -> Result<(), RegistryError> {
327 tracing::info!("Phase: init");
328
329 for entry in self.registry.gears_by_system_priority() {
330 let ctx =
331 self.ctx_builder
332 .for_gear(entry.name)
333 .await
334 .map_err(|e| RegistryError::Init {
335 gear: entry.name,
336 source: e,
337 })?;
338 tracing::info!(gear = entry.name, "Initializing a gear...");
339 entry
340 .core
341 .init(&ctx)
342 .await
343 .map_err(|e| RegistryError::Init {
344 gear: entry.name,
345 source: e,
346 })?;
347 tracing::info!(gear = entry.name, "Initialized a gear.");
348 }
349
350 Ok(())
351 }
352
353 async fn run_post_init_phase(&self) -> Result<(), RegistryError> {
360 tracing::info!("Phase: post_init");
361
362 let sys_ctx = SystemContext::new(
363 self.instance_id,
364 Arc::clone(&self.gear_manager),
365 Arc::clone(&self.grpc_installers),
366 );
367
368 for entry in self.registry.gears_by_system_priority() {
369 if let Some(sys_mod) = entry.caps.query::<SystemCap>() {
370 sys_mod
371 .post_init(&sys_ctx)
372 .await
373 .map_err(|e| RegistryError::PostInit {
374 gear: entry.name,
375 source: e,
376 })?;
377 }
378 }
379
380 Ok(())
381 }
382
383 async fn run_rest_phase(&self) -> Result<Router, RegistryError> {
390 tracing::info!("Phase: rest (sync)");
391
392 let mut router = Router::new();
393
394 let host_count = self
396 .registry
397 .gears()
398 .iter()
399 .filter(|e| e.caps.has::<ApiGatewayCap>())
400 .count();
401
402 match host_count {
403 0 => {
404 return if self
405 .registry
406 .gears()
407 .iter()
408 .any(|e| e.caps.has::<RestApiCap>())
409 {
410 Err(RegistryError::RestRequiresHost)
411 } else {
412 Ok(router)
413 };
414 }
415 1 => { }
416 _ => return Err(RegistryError::MultipleRestHosts),
417 }
418
419 let host_idx = self
421 .registry
422 .gears()
423 .iter()
424 .position(|e| e.caps.has::<ApiGatewayCap>())
425 .ok_or(RegistryError::RestHostNotFoundAfterValidation)?;
426 let host_entry = &self.registry.gears()[host_idx];
427 let Some(host) = host_entry.caps.query::<ApiGatewayCap>() else {
428 return Err(RegistryError::RestHostMissingFromEntry);
429 };
430 let host_ctx = self
431 .ctx_builder
432 .for_gear(host_entry.name)
433 .await
434 .map_err(|e| RegistryError::RestPrepare {
435 gear: host_entry.name,
436 source: e,
437 })?;
438
439 let registry: &dyn crate::contracts::OpenApiRegistry = host.as_registry();
441
442 let hc_registry = Arc::new(
446 crate::healthcheck::RestHealthcheckRegistry::with_cancellation(
447 host_ctx.cancellation_token().clone(),
448 ),
449 );
450
451 router = host
453 .rest_prepare(&host_ctx, router, hc_registry.clone())
454 .map_err(|source| RegistryError::RestPrepare {
455 gear: host_entry.name,
456 source,
457 })?;
458
459 for e in self.registry.gears() {
461 if let Some(rest) = e.caps.query::<RestApiCap>() {
462 let ctx = self.ctx_builder.for_gear(e.name).await.map_err(|err| {
463 RegistryError::RestRegister {
464 gear: e.name,
465 source: err,
466 }
467 })?;
468
469 router = rest
470 .register_rest(&ctx, router, registry)
471 .map_err(|source| RegistryError::RestRegister {
472 gear: e.name,
473 source,
474 })?;
475
476 if let Some(hc) = rest.healthcheck(&ctx) {
478 hc_registry.register(e.name, hc);
479 }
480 }
481 }
482
483 router = host
485 .rest_finalize(&host_ctx, router, hc_registry)
486 .map_err(|source| RegistryError::RestFinalize {
487 gear: host_entry.name,
488 source,
489 })?;
490
491 Ok(router)
492 }
493
494 async fn run_grpc_phase(&self) -> Result<(), RegistryError> {
498 tracing::info!("Phase: grpc (registration)");
499
500 if self.registry.grpc_hub.is_none() && self.registry.grpc_services.is_empty() {
502 return Ok(());
503 }
504
505 if self.registry.grpc_hub.is_none() && !self.registry.grpc_services.is_empty() {
507 return Err(RegistryError::GrpcRequiresHub);
508 }
509
510 if let Some(hub_name) = &self.registry.grpc_hub {
512 let mut gears_data = Vec::new();
513 let mut seen = HashSet::new();
514
515 for (gear_name, service_gear) in &self.registry.grpc_services {
517 let ctx = self.ctx_builder.for_gear(gear_name).await.map_err(|err| {
518 RegistryError::GrpcRegister {
519 gear: gear_name.clone(),
520 source: err,
521 }
522 })?;
523
524 let installers = service_gear
525 .get_grpc_services(&ctx)
526 .await
527 .map_err(|source| RegistryError::GrpcRegister {
528 gear: gear_name.clone(),
529 source,
530 })?;
531
532 for reg in &installers {
533 if !seen.insert(reg.service_name) {
534 return Err(RegistryError::GrpcRegister {
535 gear: gear_name.clone(),
536 source: anyhow::anyhow!(
537 "Duplicate gRPC service name: {}",
538 reg.service_name
539 ),
540 });
541 }
542 }
543
544 gears_data.push(crate::runtime::GearInstallers {
545 gear_name: gear_name.clone(),
546 installers,
547 });
548 }
549
550 self.grpc_installers
551 .set(crate::runtime::GrpcInstallerData { gears: gears_data })
552 .map_err(|source| RegistryError::GrpcRegister {
553 gear: hub_name.clone(),
554 source,
555 })?;
556 }
557
558 Ok(())
559 }
560
561 async fn run_start_phase(&self) -> Result<(), RegistryError> {
565 tracing::info!("Phase: start");
566
567 for e in self.registry.gears_by_system_priority() {
568 if let Some(s) = e.caps.query::<RunnableCap>() {
569 tracing::debug!(
570 gear = e.name,
571 is_system = e.caps.has::<SystemCap>(),
572 "Starting stateful gear"
573 );
574 s.start(self.cancel.clone())
575 .await
576 .map_err(|source| RegistryError::Start {
577 gear: e.name,
578 source,
579 })?;
580 tracing::info!(gear = e.name, "Started gear");
581 }
582 }
583
584 Ok(())
585 }
586
587 async fn stop_one_gear(entry: &GearEntry, cancel: CancellationToken) {
589 if let Some(s) = entry.caps.query::<RunnableCap>() {
590 match s.stop(cancel).await {
591 Err(err) => {
592 tracing::warn!(gear = entry.name, error = %err, "Failed to stop gear");
593 }
594 _ => {
595 tracing::info!(gear = entry.name, "Stopped gear");
596 }
597 }
598 }
599 }
600
601 async fn run_stop_phase(&self) -> Result<(), RegistryError> {
626 tracing::info!("Phase: stop");
627
628 let deadline = self.shutdown_deadline;
629
630 for e in self.registry.gears().iter().rev() {
632 let gear_name = e.name;
633
634 let deadline_token = CancellationToken::new();
637 let deadline_token_for_timeout = deadline_token.clone();
638
639 let deadline_task = tokio::spawn(async move {
641 tokio::time::sleep(deadline).await;
642 tracing::warn!(
643 gear = gear_name,
644 deadline_secs = deadline.as_secs(),
645 "Gear shutdown deadline reached, sending hard-stop signal"
646 );
647 deadline_token_for_timeout.cancel();
648 });
649
650 Self::stop_one_gear(e, deadline_token).await;
653
654 deadline_task.abort();
656 #[allow(clippy::let_underscore_must_use)]
657 let _ = deadline_task.await;
658 }
659
660 Ok(())
661 }
662
663 async fn run_oop_spawn_phase(&self) -> Result<(), RegistryError> {
668 let oop_opts = match &self.oop_options {
669 Some(opts) if !opts.gears.is_empty() => opts,
670 _ => return Ok(()),
671 };
672
673 tracing::info!("Phase: oop_spawn");
674
675 let directory_endpoint = self.wait_for_grpc_hub_endpoint().await;
677
678 for gear_cfg in &oop_opts.gears {
679 let mut env = gear_cfg.env.clone();
682 env.insert(
683 TOOLKIT_MODULE_CONFIG_ENV.to_owned(),
684 gear_cfg.rendered_config_json.clone(),
685 );
686 if let Some(ref endpoint) = directory_endpoint {
687 env.insert(TOOLKIT_DIRECTORY_ENDPOINT_ENV.to_owned(), endpoint.clone());
688 }
689
690 let args = gear_cfg.args.clone();
692
693 let spawn_config = OopSpawnConfig {
694 gear_name: gear_cfg.gear_name.clone(),
695 binary: gear_cfg.binary.clone(),
696 args,
697 env,
698 working_directory: gear_cfg.working_directory.clone(),
699 };
700
701 oop_opts
702 .backend
703 .spawn(spawn_config)
704 .await
705 .map_err(|e| RegistryError::OopSpawn {
706 gear: gear_cfg.gear_name.clone(),
707 source: e,
708 })?;
709
710 tracing::info!(
711 gear = %gear_cfg.gear_name,
712 directory_endpoint = ?directory_endpoint,
713 "Spawned OoP gear via backend"
714 );
715 }
716
717 Ok(())
718 }
719
720 async fn wait_for_grpc_hub_endpoint(&self) -> Option<String> {
725 const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
726 const MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
727
728 let grpc_hub = self
730 .registry
731 .gears()
732 .iter()
733 .find_map(|e| e.caps.query::<GrpcHubCap>());
734
735 let Some(hub) = grpc_hub else {
736 return None; };
738
739 let start = std::time::Instant::now();
740
741 loop {
742 if let Some(endpoint) = hub.bound_endpoint() {
743 tracing::debug!(
744 endpoint = %endpoint,
745 elapsed_ms = start.elapsed().as_millis(),
746 "gRPC hub endpoint available"
747 );
748 return Some(endpoint);
749 }
750
751 if start.elapsed() > MAX_WAIT {
752 tracing::warn!("Timed out waiting for gRPC hub to bind");
753 return None;
754 }
755
756 tokio::time::sleep(POLL_INTERVAL).await;
757 }
758 }
759
760 pub async fn run_gear_phases(self) -> anyhow::Result<()> {
769 self.run_phases_internal(RunMode::Full).await
770 }
771
772 pub async fn run_migration_phases(self) -> anyhow::Result<()> {
782 self.run_phases_internal(RunMode::MigrateOnly).await
783 }
784
785 async fn run_phases_internal(self, mode: RunMode) -> anyhow::Result<()> {
808 match mode {
810 RunMode::Full => {
811 tracing::info!("Running full lifecycle (all phases)");
812 }
813 RunMode::MigrateOnly => {
814 tracing::info!("Running in migration mode (pre-init + db phases only)");
815 }
816 }
817
818 self.run_pre_init_phase()?;
820
821 #[cfg(feature = "db")]
823 {
824 self.run_db_phase().await?;
825 }
826 #[cfg(not(feature = "db"))]
827 {
828 }
830
831 if mode == RunMode::MigrateOnly {
833 tracing::info!("Migration phases completed successfully");
834 return Ok(());
835 }
836
837 self.run_init_phase().await?;
839
840 self.run_post_init_phase().await?;
842
843 let _router = self.run_rest_phase().await?;
845
846 self.run_grpc_phase().await?;
848
849 self.run_start_phase().await?;
851
852 self.run_oop_spawn_phase().await?;
854
855 self.cancel.cancelled().await;
857
858 let stop_timeout = std::time::Duration::from_secs(15);
864 let disarm = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
865 let disarm_clone = std::sync::Arc::clone(&disarm);
866 std::thread::spawn(move || {
867 std::thread::sleep(stop_timeout);
868 if !disarm_clone.load(std::sync::atomic::Ordering::Relaxed) {
869 tracing::warn!(
870 timeout_secs = stop_timeout.as_secs(),
871 "shutdown: stop phase timed out, force exiting"
872 );
873 std::process::exit(1);
874 }
875 });
876
877 self.run_stop_phase().await?;
878 disarm.store(true, std::sync::atomic::Ordering::Relaxed);
879
880 Ok(())
881 }
882}
883
884#[cfg(test)]
885#[cfg_attr(coverage_nightly, coverage(off))]
886mod tests {
887 use super::*;
888 use crate::context::GearCtx;
889 use crate::contracts::{Gear, RunnableCapability, SystemCapability};
890 use crate::registry::RegistryBuilder;
891 use std::sync::Arc;
892 use std::sync::atomic::{AtomicUsize, Ordering};
893 use tokio::sync::Mutex;
894
895 #[derive(Default)]
896 #[allow(dead_code)]
897 struct DummyCore;
898 #[async_trait::async_trait]
899 impl Gear for DummyCore {
900 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
901 Ok(())
902 }
903 }
904
905 struct StopOrderTracker {
906 my_order: usize,
907 stop_order: Arc<AtomicUsize>,
908 }
909
910 impl StopOrderTracker {
911 fn new(counter: &Arc<AtomicUsize>, stop_order: Arc<AtomicUsize>) -> Self {
912 let my_order = counter.fetch_add(1, Ordering::SeqCst);
913 Self {
914 my_order,
915 stop_order,
916 }
917 }
918 }
919
920 #[async_trait::async_trait]
921 impl Gear for StopOrderTracker {
922 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
923 Ok(())
924 }
925 }
926
927 #[async_trait::async_trait]
928 impl RunnableCapability for StopOrderTracker {
929 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
930 Ok(())
931 }
932 async fn stop(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
933 let order = self.stop_order.fetch_add(1, Ordering::SeqCst);
934 tracing::info!(my_order = self.my_order, stop_order = order, "Gear stopped");
935 Ok(())
936 }
937 }
938
939 #[tokio::test]
940 async fn test_stop_phase_reverse_order() {
941 let counter = Arc::new(AtomicUsize::new(0));
942 let stop_order = Arc::new(AtomicUsize::new(0));
943
944 let gear_a = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
945 let gear_b = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
946 let gear_c = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
947
948 let mut builder = RegistryBuilder::default();
949 builder.register_core_with_meta("a", &[], gear_a.clone() as Arc<dyn Gear>);
950 builder.register_core_with_meta("b", &["a"], gear_b.clone() as Arc<dyn Gear>);
951 builder.register_core_with_meta("c", &["b"], gear_c.clone() as Arc<dyn Gear>);
952
953 builder.register_stateful_with_meta("a", gear_a.clone() as Arc<dyn RunnableCapability>);
954 builder.register_stateful_with_meta("b", gear_b.clone() as Arc<dyn RunnableCapability>);
955 builder.register_stateful_with_meta("c", gear_c.clone() as Arc<dyn RunnableCapability>);
956
957 let registry = builder.build_topo_sorted().unwrap();
958
959 let gear_names: Vec<_> = registry.gears().iter().map(|m| m.name).collect();
961 assert_eq!(gear_names, vec!["a", "b", "c"]);
962
963 let client_hub = Arc::new(ClientHub::new());
964 let cancel = CancellationToken::new();
965 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
966
967 let runtime = HostRuntime::new(
968 registry,
969 config_provider,
970 DbOptions::None,
971 client_hub,
972 cancel.clone(),
973 Uuid::new_v4(),
974 None,
975 );
976
977 runtime.run_stop_phase().await.unwrap();
979
980 assert_eq!(stop_order.load(Ordering::SeqCst), 3);
984 }
985
986 #[tokio::test]
987 async fn test_stop_phase_continues_on_error() {
988 struct FailingGear {
989 should_fail: bool,
990 stopped: Arc<AtomicUsize>,
991 }
992
993 #[async_trait::async_trait]
994 impl Gear for FailingGear {
995 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
996 Ok(())
997 }
998 }
999
1000 #[async_trait::async_trait]
1001 impl RunnableCapability for FailingGear {
1002 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1003 Ok(())
1004 }
1005 async fn stop(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1006 self.stopped.fetch_add(1, Ordering::SeqCst);
1007 if self.should_fail {
1008 anyhow::bail!("Intentional failure")
1009 }
1010 Ok(())
1011 }
1012 }
1013
1014 let stopped = Arc::new(AtomicUsize::new(0));
1015 let gear_a = Arc::new(FailingGear {
1016 should_fail: false,
1017 stopped: stopped.clone(),
1018 });
1019 let gear_b = Arc::new(FailingGear {
1020 should_fail: true,
1021 stopped: stopped.clone(),
1022 });
1023 let gear_c = Arc::new(FailingGear {
1024 should_fail: false,
1025 stopped: stopped.clone(),
1026 });
1027
1028 let mut builder = RegistryBuilder::default();
1029 builder.register_core_with_meta("a", &[], gear_a.clone() as Arc<dyn Gear>);
1030 builder.register_core_with_meta("b", &["a"], gear_b.clone() as Arc<dyn Gear>);
1031 builder.register_core_with_meta("c", &["b"], gear_c.clone() as Arc<dyn Gear>);
1032
1033 builder.register_stateful_with_meta("a", gear_a.clone() as Arc<dyn RunnableCapability>);
1034 builder.register_stateful_with_meta("b", gear_b.clone() as Arc<dyn RunnableCapability>);
1035 builder.register_stateful_with_meta("c", gear_c.clone() as Arc<dyn RunnableCapability>);
1036
1037 let registry = builder.build_topo_sorted().unwrap();
1038
1039 let client_hub = Arc::new(ClientHub::new());
1040 let cancel = CancellationToken::new();
1041 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1042
1043 let runtime = HostRuntime::new(
1044 registry,
1045 config_provider,
1046 DbOptions::None,
1047 client_hub,
1048 cancel.clone(),
1049 Uuid::new_v4(),
1050 None,
1051 );
1052
1053 runtime.run_stop_phase().await.unwrap();
1055
1056 assert_eq!(stopped.load(Ordering::SeqCst), 3);
1058 }
1059
1060 struct EmptyConfigProvider;
1061 impl ConfigProvider for EmptyConfigProvider {
1062 fn get_gear_config(&self, _gear_name: &str) -> Option<&serde_json::Value> {
1063 None
1064 }
1065 }
1066
1067 #[tokio::test]
1068 async fn test_post_init_runs_after_all_init_and_system_first() {
1069 #[derive(Clone)]
1070 struct TrackHooks {
1071 name: &'static str,
1072 events: Arc<Mutex<Vec<String>>>,
1073 }
1074
1075 #[async_trait::async_trait]
1076 impl Gear for TrackHooks {
1077 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1078 self.events.lock().await.push(format!("init:{}", self.name));
1079 Ok(())
1080 }
1081 }
1082
1083 #[async_trait::async_trait]
1084 impl SystemCapability for TrackHooks {
1085 fn pre_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1086 Ok(())
1087 }
1088
1089 async fn post_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1090 self.events
1091 .lock()
1092 .await
1093 .push(format!("post_init:{}", self.name));
1094 Ok(())
1095 }
1096 }
1097
1098 let events = Arc::new(Mutex::new(Vec::<String>::new()));
1099 let sys_a = Arc::new(TrackHooks {
1100 name: "sys_a",
1101 events: events.clone(),
1102 });
1103 let user_b = Arc::new(TrackHooks {
1104 name: "user_b",
1105 events: events.clone(),
1106 });
1107 let user_c = Arc::new(TrackHooks {
1108 name: "user_c",
1109 events: events.clone(),
1110 });
1111
1112 let mut builder = RegistryBuilder::default();
1113 builder.register_core_with_meta("sys_a", &[], sys_a.clone() as Arc<dyn Gear>);
1114 builder.register_core_with_meta("user_b", &["sys_a"], user_b.clone() as Arc<dyn Gear>);
1115 builder.register_core_with_meta("user_c", &["user_b"], user_c.clone() as Arc<dyn Gear>);
1116 builder.register_system_with_meta("sys_a", sys_a.clone() as Arc<dyn SystemCapability>);
1117
1118 let registry = builder.build_topo_sorted().unwrap();
1119
1120 let client_hub = Arc::new(ClientHub::new());
1121 let cancel = CancellationToken::new();
1122 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1123
1124 let runtime = HostRuntime::new(
1125 registry,
1126 config_provider,
1127 DbOptions::None,
1128 client_hub,
1129 cancel,
1130 Uuid::new_v4(),
1131 None,
1132 );
1133
1134 runtime.run_init_phase().await.unwrap();
1136 runtime.run_post_init_phase().await.unwrap();
1137
1138 let events = events.lock().await.clone();
1139 let first_post_init = events
1140 .iter()
1141 .position(|e| e.starts_with("post_init:"))
1142 .expect("expected post_init events");
1143 assert!(
1144 events[..first_post_init]
1145 .iter()
1146 .all(|e| e.starts_with("init:")),
1147 "expected all init events before post_init, got: {events:?}"
1148 );
1149
1150 assert_eq!(
1152 events,
1153 vec![
1154 "init:sys_a",
1155 "init:user_b",
1156 "init:user_c",
1157 "post_init:sys_a",
1158 ]
1159 );
1160 }
1161
1162 #[tokio::test]
1163 async fn test_stop_phase_provides_fresh_deadline_token() {
1164 use std::sync::atomic::AtomicBool;
1165
1166 struct TokenCheckGear {
1167 stop_was_called: AtomicBool,
1168 token_was_cancelled_on_entry: AtomicBool,
1169 }
1170
1171 #[async_trait::async_trait]
1172 impl Gear for TokenCheckGear {
1173 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1174 Ok(())
1175 }
1176 }
1177
1178 #[async_trait::async_trait]
1179 impl RunnableCapability for TokenCheckGear {
1180 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1181 Ok(())
1182 }
1183 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
1184 self.stop_was_called.store(true, Ordering::SeqCst);
1186 self.token_was_cancelled_on_entry
1188 .store(deadline_token.is_cancelled(), Ordering::SeqCst);
1189 Ok(())
1190 }
1191 }
1192
1193 let gear = Arc::new(TokenCheckGear {
1194 stop_was_called: AtomicBool::new(false),
1195 token_was_cancelled_on_entry: AtomicBool::new(true),
1197 });
1198
1199 let mut builder = RegistryBuilder::default();
1200 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
1201 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
1202
1203 let registry = builder.build_topo_sorted().unwrap();
1204 let client_hub = Arc::new(ClientHub::new());
1205 let cancel = CancellationToken::new();
1206 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1207
1208 let runtime = HostRuntime::new(
1209 registry,
1210 config_provider,
1211 DbOptions::None,
1212 client_hub,
1213 cancel.clone(),
1214 Uuid::new_v4(),
1215 None,
1216 );
1217
1218 runtime.run_stop_phase().await.unwrap();
1220
1221 assert!(
1223 gear.stop_was_called.load(Ordering::SeqCst),
1224 "stop() was never called - gear may not have been registered correctly"
1225 );
1226
1227 assert!(
1230 !gear.token_was_cancelled_on_entry.load(Ordering::SeqCst),
1231 "deadline_token should NOT be cancelled when stop() is called - this enables graceful shutdown"
1232 );
1233 }
1234
1235 #[tokio::test]
1236 async fn test_stop_phase_graceful_shutdown_completes_before_deadline() {
1237 use std::sync::atomic::AtomicBool;
1238 use std::time::Duration;
1239
1240 struct GracefulGear {
1241 graceful_completed: AtomicBool,
1242 deadline_fired: AtomicBool,
1243 }
1244
1245 #[async_trait::async_trait]
1246 impl Gear for GracefulGear {
1247 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1248 Ok(())
1249 }
1250 }
1251
1252 #[async_trait::async_trait]
1253 impl RunnableCapability for GracefulGear {
1254 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1255 Ok(())
1256 }
1257 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
1258 tokio::select! {
1260 () = tokio::time::sleep(Duration::from_millis(10)) => {
1261 self.graceful_completed.store(true, Ordering::SeqCst);
1262 }
1263 () = deadline_token.cancelled() => {
1264 self.deadline_fired.store(true, Ordering::SeqCst);
1265 }
1266 }
1267 Ok(())
1268 }
1269 }
1270
1271 let gear = Arc::new(GracefulGear {
1272 graceful_completed: AtomicBool::new(false),
1273 deadline_fired: AtomicBool::new(false),
1274 });
1275
1276 let mut builder = RegistryBuilder::default();
1277 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
1278 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
1279
1280 let registry = builder.build_topo_sorted().unwrap();
1281 let client_hub = Arc::new(ClientHub::new());
1282 let cancel = CancellationToken::new();
1283 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1284
1285 let runtime = HostRuntime::new(
1287 registry,
1288 config_provider,
1289 DbOptions::None,
1290 client_hub,
1291 cancel.clone(),
1292 Uuid::new_v4(),
1293 None,
1294 )
1295 .with_shutdown_deadline(Duration::from_secs(5));
1296
1297 runtime.run_stop_phase().await.unwrap();
1298
1299 assert!(
1301 gear.graceful_completed.load(Ordering::SeqCst),
1302 "graceful shutdown should complete"
1303 );
1304 assert!(
1306 !gear.deadline_fired.load(Ordering::SeqCst),
1307 "deadline should not fire when graceful shutdown completes quickly"
1308 );
1309 }
1310
1311 #[tokio::test]
1312 async fn test_stop_phase_deadline_fires_for_slow_gear() {
1313 use std::sync::atomic::AtomicBool;
1314 use std::time::Duration;
1315
1316 struct SlowGear {
1317 graceful_completed: AtomicBool,
1318 deadline_fired: AtomicBool,
1319 }
1320
1321 #[async_trait::async_trait]
1322 impl Gear for SlowGear {
1323 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1324 Ok(())
1325 }
1326 }
1327
1328 #[async_trait::async_trait]
1329 impl RunnableCapability for SlowGear {
1330 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1331 Ok(())
1332 }
1333 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
1334 tokio::select! {
1336 () = tokio::time::sleep(Duration::from_secs(10)) => {
1337 self.graceful_completed.store(true, Ordering::SeqCst);
1338 }
1339 () = deadline_token.cancelled() => {
1340 self.deadline_fired.store(true, Ordering::SeqCst);
1341 }
1342 }
1343 Ok(())
1344 }
1345 }
1346
1347 let gear = Arc::new(SlowGear {
1348 graceful_completed: AtomicBool::new(false),
1349 deadline_fired: AtomicBool::new(false),
1350 });
1351
1352 let mut builder = RegistryBuilder::default();
1353 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
1354 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
1355
1356 let registry = builder.build_topo_sorted().unwrap();
1357 let client_hub = Arc::new(ClientHub::new());
1358 let cancel = CancellationToken::new();
1359 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1360
1361 let runtime = HostRuntime::new(
1363 registry,
1364 config_provider,
1365 DbOptions::None,
1366 client_hub,
1367 cancel.clone(),
1368 Uuid::new_v4(),
1369 None,
1370 )
1371 .with_shutdown_deadline(Duration::from_millis(100));
1372
1373 runtime.run_stop_phase().await.unwrap();
1374
1375 assert!(
1377 !gear.graceful_completed.load(Ordering::SeqCst),
1378 "graceful shutdown should not complete when deadline fires first"
1379 );
1380 assert!(
1382 gear.deadline_fired.load(Ordering::SeqCst),
1383 "deadline should fire for slow gears"
1384 );
1385 }
1386}