1use axum::Router;
25use std::collections::HashSet;
26use std::sync::Arc;
27
28use tokio_util::sync::CancellationToken;
29use uuid::Uuid;
30
31use crate::backends::OopSpawnConfig;
32use crate::client_hub::ClientHub;
33use crate::config::ConfigProvider;
34use crate::context::GearContextBuilder;
35use crate::registry::{
36 ApiGatewayCap, GearEntry, GearRegistry, GrpcHubCap, RegistryError, RestApiCap, RunnableCap,
37 SystemCap,
38};
39use crate::runtime::{GearManager, GrpcInstallerStore, OopSpawnOptions, SystemContext};
40
41#[cfg(feature = "db")]
42use crate::registry::DatabaseCap;
43
44#[derive(Clone)]
46pub enum DbOptions {
47 None,
49 #[cfg(feature = "db")]
51 Manager(Arc<toolkit_db::DbManager>),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum RunMode {
57 Full,
59 MigrateOnly,
61}
62
63pub const TOOLKIT_DIRECTORY_ENDPOINT_ENV: &str = "TOOLKIT_DIRECTORY_ENDPOINT";
65
66pub const TOOLKIT_MODULE_CONFIG_ENV: &str = "TOOLKIT_MODULE_CONFIG";
68
69pub const DEFAULT_SHUTDOWN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(35);
75
76fn static_endpoint_override(
84 cfg: &dyn ConfigProvider,
85 owner_gear: &str,
86 dep_gear: &str,
87) -> Option<String> {
88 cfg.get_gear_config(owner_gear)?
89 .get("config")?
90 .get("consumer_wiring")?
91 .get(dep_gear)?
92 .as_str()
93 .map(str::to_owned)
94}
95
96pub struct HostRuntime {
97 registry: GearRegistry,
98 ctx_builder: GearContextBuilder,
99 instance_id: Uuid,
100 gear_manager: Arc<GearManager>,
101 grpc_installers: Arc<GrpcInstallerStore>,
102 client_hub: Arc<ClientHub>,
103 gears_cfg: Arc<dyn ConfigProvider>,
106 dep_checker: Arc<super::readiness::DependencyChecker>,
112 rest_providers_registered: std::sync::atomic::AtomicBool,
118 cancel: CancellationToken,
119 #[allow(dead_code)]
120 db_options: DbOptions,
121 oop_options: Option<OopSpawnOptions>,
123 shutdown_deadline: std::time::Duration,
125}
126
127impl HostRuntime {
128 pub fn new(
132 registry: GearRegistry,
133 gears_cfg: Arc<dyn ConfigProvider>,
134 db_options: DbOptions,
135 client_hub: Arc<ClientHub>,
136 cancel: CancellationToken,
137 instance_id: Uuid,
138 oop_options: Option<OopSpawnOptions>,
139 ) -> Self {
140 let gear_manager = Arc::new(GearManager::new());
142 let grpc_installers = Arc::new(GrpcInstallerStore::new());
143
144 let dep_checker = Arc::new(super::readiness::DependencyChecker::new());
148 client_hub.register::<super::readiness::DependencyChecker>(dep_checker.clone());
149
150 let ctx_builder = GearContextBuilder::new(
152 instance_id,
153 gears_cfg.clone(),
154 client_hub.clone(),
155 cancel.clone(),
156 );
157 #[cfg(feature = "db")]
158 let ctx_builder = match &db_options {
159 DbOptions::Manager(mgr) => ctx_builder.with_db_manager(mgr.clone()),
160 DbOptions::None => ctx_builder,
161 };
162
163 Self {
164 registry,
165 ctx_builder,
166 instance_id,
167 gear_manager,
168 grpc_installers,
169 client_hub,
170 gears_cfg,
171 dep_checker,
172 rest_providers_registered: std::sync::atomic::AtomicBool::new(false),
173 cancel,
174 db_options,
175 oop_options,
176 shutdown_deadline: DEFAULT_SHUTDOWN_DEADLINE,
177 }
178 }
179
180 #[must_use]
197 pub fn with_shutdown_deadline(mut self, deadline: std::time::Duration) -> Self {
198 self.shutdown_deadline = deadline;
199 self
200 }
201
202 #[must_use]
208 pub fn with_internal_token_provider(
209 mut self,
210 provider: Option<toolkit_contract::runtime::config::InternalTokenProvider>,
211 ) -> Self {
212 self.ctx_builder = self.ctx_builder.with_internal_token_provider(provider);
213 self
214 }
215
216 pub fn run_pre_init_phase(&self) -> Result<(), RegistryError> {
223 tracing::info!("Phase: pre_init");
224
225 let sys_ctx = SystemContext::new(
226 self.instance_id,
227 Arc::clone(&self.gear_manager),
228 Arc::clone(&self.grpc_installers),
229 );
230
231 for entry in self.registry.gears() {
232 if self.cancel.is_cancelled() {
234 tracing::warn!("Pre-init phase cancelled by signal");
235 return Err(RegistryError::Cancelled);
236 }
237
238 if let Some(sys_mod) = entry.caps.query::<SystemCap>() {
239 tracing::debug!(gear = entry.name, "Running system pre_init");
240 sys_mod
241 .pre_init(&sys_ctx)
242 .map_err(|e| RegistryError::PreInit {
243 gear: entry.name,
244 source: e,
245 })?;
246 }
247 }
248
249 Ok(())
250 }
251
252 #[cfg(feature = "db")]
254 async fn gear_context(
255 &self,
256 gear_name: &'static str,
257 ) -> Result<crate::context::GearCtx, RegistryError> {
258 self.ctx_builder
259 .for_gear(gear_name)
260 .await
261 .map_err(|e| RegistryError::DbMigrate {
262 gear: gear_name,
263 source: e,
264 })
265 }
266
267 #[cfg(feature = "db")]
269 async fn db_migration_target(
270 &self,
271 gear_name: &'static str,
272 ctx: &crate::context::GearCtx,
273 db_gear: Option<Arc<dyn crate::contracts::DatabaseCapability>>,
274 ) -> Result<
275 Option<(
276 toolkit_db::Db,
277 Arc<dyn crate::contracts::DatabaseCapability>,
278 )>,
279 RegistryError,
280 > {
281 let Some(dbm) = db_gear else {
282 return Ok(None);
283 };
284
285 let db = match &self.db_options {
289 DbOptions::None => None,
290 #[cfg(feature = "db")]
291 DbOptions::Manager(mgr) => {
292 mgr.get(gear_name)
293 .await
294 .map_err(|e| RegistryError::DbMigrate {
295 gear: gear_name,
296 source: e.into(),
297 })?
298 }
299 };
300
301 _ = ctx; Ok(db.map(|db| (db, dbm)))
303 }
304
305 #[cfg(feature = "db")]
310 async fn migrate_gear(
311 gear_name: &'static str,
312 db: &toolkit_db::Db,
313 db_gear: Arc<dyn crate::contracts::DatabaseCapability>,
314 ) -> Result<(), RegistryError> {
315 let migrations = db_gear.migrations();
317
318 if migrations.is_empty() {
319 tracing::debug!(gear = gear_name, "No migrations to run");
320 return Ok(());
321 }
322
323 tracing::debug!(
324 gear = gear_name,
325 count = migrations.len(),
326 "Running DB migrations"
327 );
328
329 let result =
331 toolkit_db::migration_runner::run_migrations_for_gear(db, gear_name, migrations)
332 .await
333 .map_err(|e| RegistryError::DbMigrate {
334 gear: gear_name,
335 source: anyhow::Error::new(e),
336 })?;
337
338 tracing::info!(
339 gear = gear_name,
340 applied = result.applied,
341 skipped = result.skipped,
342 "DB migrations completed"
343 );
344
345 Ok(())
346 }
347
348 #[cfg(feature = "db")]
357 async fn run_db_phase(&self) -> Result<(), RegistryError> {
358 tracing::info!("Phase: db (before init)");
359
360 for entry in self.registry.gears_by_system_priority() {
361 if self.cancel.is_cancelled() {
363 tracing::warn!("DB migration phase cancelled by signal");
364 return Err(RegistryError::Cancelled);
365 }
366
367 let ctx = self.gear_context(entry.name).await?;
368 let db_gear = entry.caps.query::<DatabaseCap>();
369
370 match self
371 .db_migration_target(entry.name, &ctx, db_gear.clone())
372 .await?
373 {
374 Some((db, dbm)) => {
375 Self::migrate_gear(entry.name, &db, dbm).await?;
376 }
377 None if db_gear.is_some() => {
378 tracing::debug!(
379 gear = entry.name,
380 "Gear has DbGear trait but no DB handle (no config)"
381 );
382 }
383 None => {}
384 }
385 }
386
387 Ok(())
388 }
389
390 async fn run_init_phase(&self) -> Result<(), RegistryError> {
394 tracing::info!("Phase: init");
395
396 for entry in self.registry.gears_by_system_priority() {
397 let ctx =
398 self.ctx_builder
399 .for_gear(entry.name)
400 .await
401 .map_err(|e| RegistryError::Init {
402 gear: entry.name,
403 source: e,
404 })?;
405 tracing::info!(gear = entry.name, "Initializing a gear...");
406 entry
407 .core
408 .init(&ctx)
409 .await
410 .map_err(|e| RegistryError::Init {
411 gear: entry.name,
412 source: e,
413 })?;
414 tracing::info!(gear = entry.name, "Initialized a gear.");
415 }
416
417 Ok(())
418 }
419
420 #[allow(
435 clippy::unused_async,
436 reason = "kept async for symmetry with the other `run_*_phase` steps awaited in sequence by `run_gear_phases`; the awaited work runs in a spawned readiness-probe task"
437 )]
438 async fn run_proxy_wiring_phase(&self) -> Result<(), RegistryError> {
439 use crate::discovery::{
440 ConsumerRegistration, DirectoryEndpointResolver, NullEndpointResolver,
441 };
442 use toolkit_contract::runtime::resolving::EndpointResolver;
443
444 let regs: Vec<&ConsumerRegistration> = inventory::iter::<ConsumerRegistration>
445 .into_iter()
446 .collect();
447 if regs.is_empty() {
448 return Ok(());
449 }
450 tracing::info!(
451 count = regs.len(),
452 "Phase: proxy-wiring (consumer discovery)"
453 );
454
455 let (resolver, have_directory): (Arc<dyn EndpointResolver>, bool) =
462 if let Ok(dir) = self.client_hub.get::<dyn crate::DirectoryClient>() {
463 (Arc::new(DirectoryEndpointResolver::new(dir)), true)
464 } else {
465 tracing::error!(
466 consumers = regs.len(),
467 "proxy-wiring: no DirectoryClient in ClientHub; remote consumer \
468 dependencies cannot be resolved and will gate /readyz (503). \
469 Co-located (local) dependencies are unaffected."
470 );
471 (Arc::new(NullEndpointResolver), false)
472 };
473
474 let known_gears: std::collections::HashSet<&str> =
486 self.registry.gears().iter().map(GearEntry::name).collect();
487 for reg in ®s {
488 if !known_gears.contains(reg.owner_gear) {
489 tracing::warn!(
490 owner = reg.owner_gear,
491 dep = reg.dep_gear,
492 "proxy-wiring: consumer's owner gear name does not match any registered gear; \
493 the `gears.{}.config.consumer_wiring.{}` static override will never resolve. \
494 Rename the gear to the kebab-case of its struct ident.",
495 reg.owner_gear,
496 reg.dep_gear,
497 );
498 }
499 }
500
501 let mut remote_deps: Vec<String> = Vec::new();
502 for reg in ®s {
503 let static_override =
509 static_endpoint_override(self.gears_cfg.as_ref(), reg.owner_gear, reg.dep_gear);
510 let (reg_resolver, is_static): (Arc<dyn EndpointResolver>, bool) =
511 if let Some(endpoint) = &static_override {
512 tracing::warn!(
513 owner = reg.owner_gear,
514 dep = reg.dep_gear,
515 endpoint = %endpoint,
516 "proxy-wiring: STATIC endpoint override in use (ADR-0004 dev/test \
517 escape hatch) - bypasses service discovery; MUST NOT be used in \
518 production"
519 );
520 (
521 Arc::new(crate::discovery::StaticEndpointResolver::new(
522 endpoint.clone(),
523 )),
524 true,
525 )
526 } else {
527 (Arc::clone(&resolver), false)
528 };
529
530 let outcome = (reg.wire)(
536 &self.client_hub,
537 reg_resolver,
538 self.ctx_builder.internal_token_provider(),
539 )
540 .map_err(|source| RegistryError::ProxyWiring {
541 gear: reg.owner_gear,
542 source,
543 })?;
544 self.dep_checker.register_dep(reg.dep_gear.to_owned());
545 match outcome {
546 crate::discovery::WireOutcome::Local => {
548 self.dep_checker.mark_resolved(reg.dep_gear);
549 }
550 crate::discovery::WireOutcome::Remote if is_static => {
552 self.dep_checker.mark_resolved(reg.dep_gear);
553 }
554 crate::discovery::WireOutcome::Remote => remote_deps.push(reg.dep_gear.to_owned()),
556 }
557 tracing::debug!(
558 owner = reg.owner_gear,
559 dep = reg.dep_gear,
560 outcome = ?outcome,
561 static_override = is_static,
562 "wired consumer contract"
563 );
564 }
565
566 if !have_directory || remote_deps.is_empty() {
570 return Ok(());
571 }
572
573 let readiness = Arc::clone(&self.dep_checker);
574 let cancel = self.cancel.clone();
575 tokio::spawn(async move {
576 const BASE: std::time::Duration = std::time::Duration::from_millis(100);
577 const MAX: std::time::Duration = std::time::Duration::from_secs(30);
578 let mut pending = remote_deps;
579 let mut backoff = BASE;
580 while !pending.is_empty() {
581 let mut still_pending = Vec::new();
582 for dep in pending {
583 match resolver.resolve_endpoint(&dep).await {
584 Ok(Some(_)) => {
585 readiness.mark_resolved(&dep);
586 tracing::info!(dep = %dep, "readiness: dependency resolved");
587 }
588 Ok(None) => still_pending.push(dep),
590 Err(e) => {
593 tracing::warn!(dep = %dep, error = %e, "readiness: directory lookup failed");
594 still_pending.push(dep);
595 }
596 }
597 }
598 pending = still_pending;
599 if pending.is_empty() {
600 break;
601 }
602 tokio::select! {
603 () = cancel.cancelled() => break,
604 () = tokio::time::sleep(backoff) => {}
605 }
606 backoff = (backoff * 2).min(MAX);
607 }
608 });
609
610 Ok(())
611 }
612
613 async fn run_init_wiring_post_init(&self) -> Result<(), RegistryError> {
632 self.run_init_phase().await?;
633
634 self.run_proxy_wiring_phase().await?;
635
636 self.run_post_init_phase().await
637 }
638
639 async fn run_post_init_phase(&self) -> Result<(), RegistryError> {
642 tracing::info!("Phase: post_init");
643
644 let sys_ctx = SystemContext::new(
645 self.instance_id,
646 Arc::clone(&self.gear_manager),
647 Arc::clone(&self.grpc_installers),
648 );
649
650 for entry in self.registry.gears_by_system_priority() {
651 if let Some(sys_mod) = entry.caps.query::<SystemCap>() {
652 sys_mod
653 .post_init(&sys_ctx)
654 .await
655 .map_err(|e| RegistryError::PostInit {
656 gear: entry.name,
657 source: e,
658 })?;
659 }
660 }
661
662 Ok(())
663 }
664
665 async fn run_rest_phase(&self) -> Result<Router, RegistryError> {
672 tracing::info!("Phase: rest (sync)");
673
674 let mut router = Router::new();
675
676 let host_count = self
678 .registry
679 .gears()
680 .iter()
681 .filter(|e| e.caps.has::<ApiGatewayCap>())
682 .count();
683
684 match host_count {
685 0 => {
686 return if self
687 .registry
688 .gears()
689 .iter()
690 .any(|e| e.caps.has::<RestApiCap>())
691 {
692 Err(RegistryError::RestRequiresHost)
693 } else {
694 Ok(router)
695 };
696 }
697 1 => { }
698 _ => return Err(RegistryError::MultipleRestHosts),
699 }
700
701 let host_idx = self
703 .registry
704 .gears()
705 .iter()
706 .position(|e| e.caps.has::<ApiGatewayCap>())
707 .ok_or(RegistryError::RestHostNotFoundAfterValidation)?;
708 let host_entry = &self.registry.gears()[host_idx];
709 let Some(host) = host_entry.caps.query::<ApiGatewayCap>() else {
710 return Err(RegistryError::RestHostMissingFromEntry);
711 };
712 let host_ctx = self
713 .ctx_builder
714 .for_gear(host_entry.name)
715 .await
716 .map_err(|e| RegistryError::RestPrepare {
717 gear: host_entry.name,
718 source: e,
719 })?;
720
721 let registry: &dyn crate::contracts::OpenApiRegistry = host.as_registry();
723
724 let hc_registry = Arc::new(
728 crate::healthcheck::RestHealthcheckRegistry::with_cancellation(
729 host_ctx.cancellation_token().clone(),
730 ),
731 );
732
733 hc_registry.register(
739 "readiness",
740 Arc::new(super::readiness::ReadinessHealthcheck::new(
741 self.dep_checker.clone(),
742 )),
743 );
744
745 router = host
747 .rest_prepare(&host_ctx, router, hc_registry.clone())
748 .map_err(|source| RegistryError::RestPrepare {
749 gear: host_entry.name,
750 source,
751 })?;
752
753 for e in self.registry.gears() {
755 if let Some(rest) = e.caps.query::<RestApiCap>() {
756 let ctx = self.ctx_builder.for_gear(e.name).await.map_err(|err| {
757 RegistryError::RestRegister {
758 gear: e.name,
759 source: err,
760 }
761 })?;
762
763 router = rest
764 .register_rest(&ctx, router, registry)
765 .map_err(|source| RegistryError::RestRegister {
766 gear: e.name,
767 source,
768 })?;
769
770 if let Some(hc) = rest.healthcheck(&ctx) {
772 hc_registry.register(e.name, hc);
773 }
774 }
775 }
776
777 router = host
779 .rest_finalize(&host_ctx, router, hc_registry)
780 .map_err(|source| RegistryError::RestFinalize {
781 gear: host_entry.name,
782 source,
783 })?;
784
785 Ok(router)
786 }
787
788 async fn run_grpc_phase(&self) -> Result<(), RegistryError> {
792 tracing::info!("Phase: grpc (registration)");
793
794 if self.registry.grpc_hub.is_none() && self.registry.grpc_services.is_empty() {
796 return Ok(());
797 }
798
799 if self.registry.grpc_hub.is_none() && !self.registry.grpc_services.is_empty() {
801 return Err(RegistryError::GrpcRequiresHub);
802 }
803
804 if let Some(hub_name) = &self.registry.grpc_hub {
806 let mut gears_data = Vec::new();
807 let mut seen = HashSet::new();
808 let mut owners: std::collections::HashMap<String, String> =
812 std::collections::HashMap::new();
813
814 for (gear_name, service_gear) in &self.registry.grpc_services {
816 let ctx = self.ctx_builder.for_gear(gear_name).await.map_err(|err| {
817 RegistryError::GrpcRegister {
818 gear: gear_name.clone(),
819 source: err,
820 }
821 })?;
822
823 let installers = service_gear
824 .get_grpc_services(&ctx)
825 .await
826 .map_err(|source| RegistryError::GrpcRegister {
827 gear: gear_name.clone(),
828 source,
829 })?;
830
831 for reg in &installers {
832 if !seen.insert(reg.service_name) {
833 return Err(RegistryError::GrpcRegister {
834 gear: gear_name.clone(),
835 source: anyhow::anyhow!(
836 "Duplicate gRPC service name: {}",
837 reg.service_name
838 ),
839 });
840 }
841 owners.insert(reg.service_name.to_owned(), gear_name.clone());
842 }
843
844 gears_data.push(crate::runtime::GearInstallers {
845 gear_name: gear_name.clone(),
846 installers,
847 });
848 }
849
850 self.gear_manager
854 .merge_authoritative_grpc_service_owners(owners);
855
856 self.grpc_installers
857 .set(crate::runtime::GrpcInstallerData { gears: gears_data })
858 .map_err(|source| RegistryError::GrpcRegister {
859 gear: hub_name.clone(),
860 source,
861 })?;
862 }
863
864 Ok(())
865 }
866
867 async fn run_start_phase(&self) -> Result<(), RegistryError> {
871 tracing::info!("Phase: start");
872
873 for e in self.registry.gears_by_system_priority() {
874 if let Some(s) = e.caps.query::<RunnableCap>() {
875 tracing::debug!(
876 gear = e.name,
877 is_system = e.caps.has::<SystemCap>(),
878 "Starting stateful gear"
879 );
880 s.start(self.cancel.clone())
881 .await
882 .map_err(|source| RegistryError::Start {
883 gear: e.name,
884 source,
885 })?;
886 tracing::info!(gear = e.name, "Started gear");
887 }
888 }
889
890 Ok(())
891 }
892
893 async fn stop_one_gear(entry: &GearEntry, cancel: CancellationToken) {
895 if let Some(s) = entry.caps.query::<RunnableCap>() {
896 match s.stop(cancel).await {
897 Err(err) => {
898 tracing::warn!(gear = entry.name, error = %err, "Failed to stop gear");
899 }
900 _ => {
901 tracing::info!(gear = entry.name, "Stopped gear");
902 }
903 }
904 }
905 }
906
907 async fn run_stop_phase(&self) -> Result<(), RegistryError> {
932 tracing::info!("Phase: stop");
933
934 self.deregister_rest_providers().await;
937
938 let deadline = self.shutdown_deadline;
939
940 for e in self.registry.gears().iter().rev() {
942 let gear_name = e.name;
943
944 let deadline_token = CancellationToken::new();
947 let deadline_token_for_timeout = deadline_token.clone();
948
949 let deadline_task = tokio::spawn(async move {
951 tokio::time::sleep(deadline).await;
952 tracing::warn!(
953 gear = gear_name,
954 deadline_secs = deadline.as_secs(),
955 "Gear shutdown deadline reached, sending hard-stop signal"
956 );
957 deadline_token_for_timeout.cancel();
958 });
959
960 Self::stop_one_gear(e, deadline_token).await;
963
964 deadline_task.abort();
966 #[allow(clippy::let_underscore_must_use)]
967 let _ = deadline_task.await;
968 }
969
970 Ok(())
971 }
972
973 async fn run_stop_phase_guarded(&self) -> Result<(), RegistryError> {
978 let gear_count = u32::try_from(self.registry.gears().len().max(1)).unwrap_or(1);
979 let stop_timeout = self
980 .shutdown_deadline
981 .checked_mul(gear_count)
982 .and_then(|d| d.checked_add(std::time::Duration::from_secs(5)))
983 .unwrap_or(self.shutdown_deadline);
984
985 let (disarm_tx, disarm_rx) = std::sync::mpsc::channel::<()>();
989 std::thread::spawn(move || {
990 match disarm_rx.recv_timeout(stop_timeout) {
991 Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
992 }
994 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
995 tracing::warn!(
996 timeout_secs = stop_timeout.as_secs(),
997 "shutdown: stop phase timed out, force exiting"
998 );
999 std::process::exit(1);
1000 }
1001 }
1002 });
1003
1004 let stop_result = self.run_stop_phase().await;
1005 let _ = disarm_tx.send(()).ok();
1009
1010 stop_result
1011 }
1012
1013 async fn run_oop_spawn_phase(&self) -> Result<(), RegistryError> {
1018 let oop_opts = match &self.oop_options {
1019 Some(opts) if !opts.gears.is_empty() => opts,
1020 _ => return Ok(()),
1021 };
1022
1023 tracing::info!("Phase: oop_spawn");
1024
1025 let directory_endpoint = self.wait_for_grpc_hub_endpoint().await;
1027
1028 for gear_cfg in &oop_opts.gears {
1029 let mut env = gear_cfg.env.clone();
1032 env.insert(
1033 TOOLKIT_MODULE_CONFIG_ENV.to_owned(),
1034 gear_cfg.rendered_config_json.clone(),
1035 );
1036 if let Some(ref endpoint) = directory_endpoint {
1037 env.insert(TOOLKIT_DIRECTORY_ENDPOINT_ENV.to_owned(), endpoint.clone());
1038 }
1039
1040 let args = gear_cfg.args.clone();
1042
1043 let spawn_config = OopSpawnConfig {
1044 gear_name: gear_cfg.gear_name.clone(),
1045 binary: gear_cfg.binary.clone(),
1046 args,
1047 env,
1048 working_directory: gear_cfg.working_directory.clone(),
1049 };
1050
1051 oop_opts
1052 .backend
1053 .spawn(spawn_config)
1054 .await
1055 .map_err(|e| RegistryError::OopSpawn {
1056 gear: gear_cfg.gear_name.clone(),
1057 source: e,
1058 })?;
1059
1060 tracing::info!(
1061 gear = %gear_cfg.gear_name,
1062 directory_endpoint = ?directory_endpoint,
1063 "Spawned OoP gear via backend"
1064 );
1065 }
1066
1067 Ok(())
1068 }
1069
1070 async fn wait_for_grpc_hub_endpoint(&self) -> Option<String> {
1075 const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
1076 const MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
1077
1078 let grpc_hub = self
1080 .registry
1081 .gears()
1082 .iter()
1083 .find_map(|e| e.caps.query::<GrpcHubCap>());
1084
1085 let Some(hub) = grpc_hub else {
1086 return None; };
1088
1089 let start = std::time::Instant::now();
1090
1091 loop {
1092 if let Some(endpoint) = hub.bound_endpoint() {
1093 tracing::debug!(
1094 endpoint = %endpoint,
1095 elapsed_ms = start.elapsed().as_millis(),
1096 "gRPC hub endpoint available"
1097 );
1098 return Some(endpoint);
1099 }
1100
1101 if start.elapsed() > MAX_WAIT {
1102 tracing::warn!("Timed out waiting for gRPC hub to bind");
1103 return None;
1104 }
1105
1106 tokio::time::sleep(POLL_INTERVAL).await;
1107 }
1108 }
1109
1110 async fn wait_for_rest_endpoint(
1116 &self,
1117 host: &Arc<dyn crate::contracts::ApiGatewayCapability>,
1118 ) -> Option<String> {
1119 const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
1120 const MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
1121
1122 let start = std::time::Instant::now();
1123 loop {
1124 if let Some(endpoint) = host.bound_endpoint() {
1125 return Some(endpoint);
1126 }
1127 if start.elapsed() > MAX_WAIT {
1128 tracing::warn!("Timed out waiting for REST host to bind");
1129 return None;
1130 }
1131 tokio::time::sleep(POLL_INTERVAL).await;
1132 }
1133 }
1134
1135 async fn run_directory_register_phase(&self) -> Result<(), RegistryError> {
1148 let rest_gears = self.rest_provider_gears();
1149 if rest_gears.is_empty() {
1150 return Ok(());
1151 }
1152
1153 let Some(host) = self
1154 .registry
1155 .gears()
1156 .iter()
1157 .find_map(|e| e.caps.query::<ApiGatewayCap>())
1158 else {
1159 return Ok(()); };
1161
1162 let Some(endpoint) = self.wait_for_rest_endpoint(&host).await else {
1163 tracing::warn!(
1164 "directory-register: REST host endpoint unavailable; skipping REST provider registration"
1165 );
1166 return Ok(());
1167 };
1168
1169 let Ok(dir) = self.client_hub.get::<dyn crate::DirectoryClient>() else {
1170 tracing::debug!(
1171 "directory-register: no DirectoryClient in ClientHub; skipping REST provider registration"
1172 );
1173 return Ok(());
1174 };
1175
1176 let instance_id = self.instance_id.to_string();
1177 for gear in rest_gears {
1178 let (grpc_services, version) = match dir.list_instances(gear).await {
1192 Ok(insts) => insts
1193 .into_iter()
1194 .find(|i| i.instance_id == instance_id)
1195 .map(|i| (i.grpc_services, i.version))
1196 .unwrap_or_default(),
1197 Err(e) => {
1198 tracing::warn!(
1202 gear,
1203 error = %e,
1204 "directory-register: failed to read existing registration; \
1205 re-registering with empty grpc_services/version"
1206 );
1207 (Vec::new(), None)
1208 }
1209 };
1210 let mut info = crate::RegisterInstanceInfo::new(gear.to_owned(), instance_id.clone())
1214 .with_grpc_services(grpc_services)
1215 .with_rest_endpoint(crate::ServiceEndpoint::new(endpoint.clone()));
1216 if let Some(version) = version {
1217 info = info.with_version(version);
1218 }
1219 match dir.register_instance(info).await {
1220 Ok(()) => {
1221 tracing::info!(gear, endpoint = %endpoint, "registered REST provider in directory");
1222 }
1223 Err(e) => {
1224 tracing::warn!(gear, error = %e, "directory-register: failed to register REST provider");
1225 }
1226 }
1227 }
1228 self.rest_providers_registered
1232 .store(true, std::sync::atomic::Ordering::SeqCst);
1233 Ok(())
1234 }
1235
1236 fn rest_provider_gears(&self) -> Vec<&'static str> {
1241 self.registry
1242 .gears()
1243 .iter()
1244 .filter(|e| e.caps.has::<RestApiCap>() && !e.caps.has::<ApiGatewayCap>())
1245 .map(|e| e.name)
1246 .collect()
1247 }
1248
1249 async fn deregister_rest_providers(&self) {
1252 if !self
1256 .rest_providers_registered
1257 .load(std::sync::atomic::Ordering::SeqCst)
1258 {
1259 return;
1260 }
1261 let rest_gears = self.rest_provider_gears();
1262 if rest_gears.is_empty() {
1263 return;
1264 }
1265 let Ok(dir) = self.client_hub.get::<dyn crate::DirectoryClient>() else {
1266 return;
1267 };
1268 let instance_id = self.instance_id.to_string();
1269 for gear in rest_gears {
1270 if let Err(e) = dir.deregister_instance(gear, &instance_id).await {
1271 tracing::warn!(gear, error = %e, "directory-deregister: failed to deregister REST provider");
1272 }
1273 }
1274 }
1275
1276 pub async fn run_gear_phases(self) -> anyhow::Result<()> {
1285 self.run_phases_internal(RunMode::Full).await
1286 }
1287
1288 pub async fn run_migration_phases(self) -> anyhow::Result<()> {
1298 self.run_phases_internal(RunMode::MigrateOnly).await
1299 }
1300
1301 async fn run_phases_internal(self, mode: RunMode) -> anyhow::Result<()> {
1324 match mode {
1326 RunMode::Full => {
1327 tracing::info!("Running full lifecycle (all phases)");
1328 }
1329 RunMode::MigrateOnly => {
1330 tracing::info!("Running in migration mode (pre-init + db phases only)");
1331 }
1332 }
1333
1334 self.run_pre_init_phase()?;
1336
1337 #[cfg(feature = "db")]
1339 {
1340 self.run_db_phase().await?;
1341 }
1342 #[cfg(not(feature = "db"))]
1343 {
1344 }
1346
1347 if mode == RunMode::MigrateOnly {
1349 tracing::info!("Migration phases completed successfully");
1350 return Ok(());
1351 }
1352
1353 self.run_init_wiring_post_init().await?;
1355
1356 let _router = self.run_rest_phase().await?;
1358
1359 self.run_grpc_phase().await?;
1361
1362 self.run_start_phase().await?;
1364
1365 {
1369 let readiness = Arc::clone(&self.dep_checker);
1370 let cancel = self.cancel.clone();
1371 tokio::spawn(async move {
1372 cancel.cancelled().await;
1373 readiness.set_draining(true);
1374 });
1375 }
1376
1377 self.run_directory_register_phase().await?;
1380
1381 self.run_oop_spawn_phase().await?;
1383
1384 self.cancel.cancelled().await;
1386
1387 self.run_stop_phase_guarded().await?;
1391 Ok(())
1392 }
1393}
1394
1395#[cfg(feature = "bootstrap")]
1397impl HostRuntime {
1398 async fn compose_oop_router(
1404 &self,
1405 options: &crate::runtime::OopServeOptions,
1406 hc_registry: &Arc<crate::healthcheck::RestHealthcheckRegistry>,
1407 ) -> anyhow::Result<(Router, String)> {
1408 use crate::api::{OpenApiInfo, OpenApiRegistryImpl};
1409 use anyhow::Context as _;
1410
1411 let registry = OpenApiRegistryImpl::new();
1412 let mut router = Router::new();
1413
1414 for entry in self.registry.gears() {
1415 if let Some(rest) = entry.caps.query::<RestApiCap>() {
1416 let ctx = self
1417 .ctx_builder
1418 .for_gear(entry.name)
1419 .await
1420 .with_context(|| format!("OoP router: build context for '{}'", entry.name))?;
1421 router = rest
1422 .register_rest(&ctx, router, ®istry)
1423 .with_context(|| format!("OoP router: register_rest for '{}'", entry.name))?;
1424
1425 if let Some(hc) = rest.healthcheck(&ctx) {
1429 hc_registry.register(entry.name, hc);
1430 }
1431 }
1432 }
1433
1434 let info = OpenApiInfo {
1435 title: options.gear_name.clone(),
1436 version: options
1437 .version
1438 .clone()
1439 .unwrap_or_else(|| "0.0.0".to_owned()),
1440 description: None,
1441 servers: vec![],
1442 };
1443 let openapi = registry
1444 .build_openapi(&info)
1445 .context("OoP router: build OpenAPI document")?;
1446 let json = serde_json::to_string(&openapi).context("OoP router: serialize OpenAPI")?;
1447
1448 Ok((router, json))
1449 }
1450
1451 pub async fn run_oop_serving(
1459 self,
1460 options: crate::runtime::OopServeOptions,
1461 ) -> anyhow::Result<()> {
1462 use crate::runtime::ReadinessState;
1463
1464 tracing::info!("Running OoP serving lifecycle");
1465
1466 if self.client_hub.get::<dyn crate::DirectoryClient>().is_err() {
1471 self.client_hub
1472 .register::<dyn crate::DirectoryClient>(Arc::clone(&options.directory));
1473 }
1474
1475 let hc_registry = Arc::new(
1479 crate::healthcheck::RestHealthcheckRegistry::with_cancellation(self.cancel.clone()),
1480 );
1481
1482 let readiness = ReadinessState::from_checker(
1489 Arc::clone(&self.dep_checker),
1490 Arc::clone(&hc_registry),
1491 options.healthcheck_timeout,
1492 );
1493
1494 let mut server = super::oop_serve::OopHttpServer::start(
1499 Arc::clone(&readiness),
1500 options,
1501 self.cancel.clone(),
1502 )
1503 .await?;
1504
1505 let mut started = false;
1511 let composed: anyhow::Result<(Router, String)> = async {
1512 self.run_pre_init_phase()?;
1513 #[cfg(feature = "db")]
1514 self.run_db_phase().await?;
1515 self.run_init_wiring_post_init().await?;
1519 self.run_grpc_phase().await?;
1520 self.run_start_phase().await?;
1521 started = true;
1522 server.resolve_bearer_authenticator(&self.client_hub);
1528 self.compose_oop_router(server.options(), &hc_registry)
1529 .await
1530 }
1531 .await;
1532
1533 let serve_result = match composed {
1534 Ok((gear_router, openapi_json)) => {
1535 server.attach(gear_router, openapi_json);
1538 server.join().await
1540 }
1541 Err(e) => {
1542 tracing::error!(error = %e, "OoP startup failed before serving gear routes");
1543 self.cancel.cancel();
1545 if let Err(join_err) = server.join().await {
1546 tracing::warn!(error = %join_err, "OoP probe server teardown after startup failure errored");
1547 }
1548 Err(e)
1549 }
1550 };
1551
1552 if started && let Err(e) = self.run_stop_phase_guarded().await {
1555 tracing::warn!(error = %e, "OoP stop phase reported an error");
1556 }
1557
1558 serve_result
1559 }
1560}
1561
1562#[cfg(test)]
1563#[cfg(feature = "bootstrap")]
1564#[cfg_attr(coverage_nightly, coverage(off))]
1565#[path = "host_runtime_oop_tests.rs"]
1566mod host_runtime_oop_tests;
1567
1568#[cfg(test)]
1569#[cfg_attr(coverage_nightly, coverage(off))]
1570mod tests {
1571 use super::*;
1572 use crate::context::GearCtx;
1573 use crate::contracts::{Gear, RunnableCapability, SystemCapability};
1574 use crate::registry::RegistryBuilder;
1575 use std::sync::Arc;
1576 use std::sync::atomic::{AtomicUsize, Ordering};
1577 use tokio::sync::Mutex;
1578
1579 #[derive(Default)]
1580 #[allow(dead_code)]
1581 struct DummyCore;
1582 #[async_trait::async_trait]
1583 impl Gear for DummyCore {
1584 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1585 Ok(())
1586 }
1587 }
1588
1589 struct StopOrderTracker {
1590 my_order: usize,
1591 stop_order: Arc<AtomicUsize>,
1592 }
1593
1594 impl StopOrderTracker {
1595 fn new(counter: &Arc<AtomicUsize>, stop_order: Arc<AtomicUsize>) -> Self {
1596 let my_order = counter.fetch_add(1, Ordering::SeqCst);
1597 Self {
1598 my_order,
1599 stop_order,
1600 }
1601 }
1602 }
1603
1604 #[async_trait::async_trait]
1605 impl Gear for StopOrderTracker {
1606 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1607 Ok(())
1608 }
1609 }
1610
1611 #[async_trait::async_trait]
1612 impl RunnableCapability for StopOrderTracker {
1613 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1614 Ok(())
1615 }
1616 async fn stop(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1617 let order = self.stop_order.fetch_add(1, Ordering::SeqCst);
1618 tracing::info!(my_order = self.my_order, stop_order = order, "Gear stopped");
1619 Ok(())
1620 }
1621 }
1622
1623 #[tokio::test]
1624 async fn test_stop_phase_reverse_order() {
1625 let counter = Arc::new(AtomicUsize::new(0));
1626 let stop_order = Arc::new(AtomicUsize::new(0));
1627
1628 let gear_a = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
1629 let gear_b = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
1630 let gear_c = Arc::new(StopOrderTracker::new(&counter, stop_order.clone()));
1631
1632 let mut builder = RegistryBuilder::default();
1633 builder.register_core_with_meta("a", &[], gear_a.clone() as Arc<dyn Gear>);
1634 builder.register_core_with_meta("b", &["a"], gear_b.clone() as Arc<dyn Gear>);
1635 builder.register_core_with_meta("c", &["b"], gear_c.clone() as Arc<dyn Gear>);
1636
1637 builder.register_stateful_with_meta("a", gear_a.clone() as Arc<dyn RunnableCapability>);
1638 builder.register_stateful_with_meta("b", gear_b.clone() as Arc<dyn RunnableCapability>);
1639 builder.register_stateful_with_meta("c", gear_c.clone() as Arc<dyn RunnableCapability>);
1640
1641 let registry = builder.build_topo_sorted().unwrap();
1642
1643 let gear_names: Vec<_> = registry.gears().iter().map(|m| m.name).collect();
1645 assert_eq!(gear_names, vec!["a", "b", "c"]);
1646
1647 let client_hub = Arc::new(ClientHub::new());
1648 let cancel = CancellationToken::new();
1649 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1650
1651 let runtime = HostRuntime::new(
1652 registry,
1653 config_provider,
1654 DbOptions::None,
1655 client_hub,
1656 cancel.clone(),
1657 Uuid::new_v4(),
1658 None,
1659 );
1660
1661 runtime.run_stop_phase().await.unwrap();
1663
1664 assert_eq!(stop_order.load(Ordering::SeqCst), 3);
1668 }
1669
1670 #[tokio::test]
1671 async fn test_stop_phase_continues_on_error() {
1672 struct FailingGear {
1673 should_fail: bool,
1674 stopped: Arc<AtomicUsize>,
1675 }
1676
1677 #[async_trait::async_trait]
1678 impl Gear for FailingGear {
1679 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1680 Ok(())
1681 }
1682 }
1683
1684 #[async_trait::async_trait]
1685 impl RunnableCapability for FailingGear {
1686 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1687 Ok(())
1688 }
1689 async fn stop(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
1690 self.stopped.fetch_add(1, Ordering::SeqCst);
1691 if self.should_fail {
1692 anyhow::bail!("Intentional failure")
1693 }
1694 Ok(())
1695 }
1696 }
1697
1698 let stopped = Arc::new(AtomicUsize::new(0));
1699 let gear_a = Arc::new(FailingGear {
1700 should_fail: false,
1701 stopped: stopped.clone(),
1702 });
1703 let gear_b = Arc::new(FailingGear {
1704 should_fail: true,
1705 stopped: stopped.clone(),
1706 });
1707 let gear_c = Arc::new(FailingGear {
1708 should_fail: false,
1709 stopped: stopped.clone(),
1710 });
1711
1712 let mut builder = RegistryBuilder::default();
1713 builder.register_core_with_meta("a", &[], gear_a.clone() as Arc<dyn Gear>);
1714 builder.register_core_with_meta("b", &["a"], gear_b.clone() as Arc<dyn Gear>);
1715 builder.register_core_with_meta("c", &["b"], gear_c.clone() as Arc<dyn Gear>);
1716
1717 builder.register_stateful_with_meta("a", gear_a.clone() as Arc<dyn RunnableCapability>);
1718 builder.register_stateful_with_meta("b", gear_b.clone() as Arc<dyn RunnableCapability>);
1719 builder.register_stateful_with_meta("c", gear_c.clone() as Arc<dyn RunnableCapability>);
1720
1721 let registry = builder.build_topo_sorted().unwrap();
1722
1723 let client_hub = Arc::new(ClientHub::new());
1724 let cancel = CancellationToken::new();
1725 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1726
1727 let runtime = HostRuntime::new(
1728 registry,
1729 config_provider,
1730 DbOptions::None,
1731 client_hub,
1732 cancel.clone(),
1733 Uuid::new_v4(),
1734 None,
1735 );
1736
1737 runtime.run_stop_phase().await.unwrap();
1739
1740 assert_eq!(stopped.load(Ordering::SeqCst), 3);
1742 }
1743
1744 struct EmptyConfigProvider;
1745 impl ConfigProvider for EmptyConfigProvider {
1746 fn get_gear_config(&self, _gear_name: &str) -> Option<&serde_json::Value> {
1747 None
1748 }
1749 }
1750
1751 #[test]
1752 fn static_endpoint_override_reads_nested_consumer_wiring_key() {
1753 struct MapCfg(std::collections::HashMap<String, serde_json::Value>);
1754 impl ConfigProvider for MapCfg {
1755 fn get_gear_config(&self, gear: &str) -> Option<&serde_json::Value> {
1756 self.0.get(gear)
1757 }
1758 }
1759 let mut map = std::collections::HashMap::new();
1760 map.insert(
1761 "orders".to_owned(),
1762 serde_json::json!({
1763 "config": { "consumer_wiring": { "billing": "http://localhost:8081" } }
1764 }),
1765 );
1766 let cfg = MapCfg(map);
1767
1768 assert_eq!(
1770 super::static_endpoint_override(&cfg, "orders", "billing").as_deref(),
1771 Some("http://localhost:8081")
1772 );
1773 assert_eq!(
1775 super::static_endpoint_override(&cfg, "orders", "inventory"),
1776 None
1777 );
1778 assert_eq!(
1779 super::static_endpoint_override(&cfg, "warehouse", "billing"),
1780 None
1781 );
1782 assert_eq!(
1783 super::static_endpoint_override(&EmptyConfigProvider, "orders", "billing"),
1784 None
1785 );
1786 }
1787
1788 #[test]
1794 fn static_endpoint_override_is_keyed_by_kebab_gear_name() {
1795 struct MapCfg(std::collections::HashMap<String, serde_json::Value>);
1796 impl ConfigProvider for MapCfg {
1797 fn get_gear_config(&self, gear: &str) -> Option<&serde_json::Value> {
1798 self.0.get(gear)
1799 }
1800 }
1801 let mut map = std::collections::HashMap::new();
1802 map.insert(
1803 "api-contracts-consumer".to_owned(),
1804 serde_json::json!({
1805 "config": { "consumer_wiring": { "api-contracts": "http://localhost:9099" } }
1806 }),
1807 );
1808 let cfg = MapCfg(map);
1809
1810 assert_eq!(
1811 super::static_endpoint_override(&cfg, "api-contracts-consumer", "api-contracts")
1812 .as_deref(),
1813 Some("http://localhost:9099"),
1814 );
1815 assert_eq!(
1817 super::static_endpoint_override(&cfg, "ApiContractsConsumer", "api-contracts"),
1818 None,
1819 );
1820 }
1821
1822 #[tokio::test]
1823 async fn test_post_init_runs_after_all_init_and_system_first() {
1824 #[derive(Clone)]
1825 struct TrackHooks {
1826 name: &'static str,
1827 events: Arc<Mutex<Vec<String>>>,
1828 }
1829
1830 #[async_trait::async_trait]
1831 impl Gear for TrackHooks {
1832 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1833 self.events.lock().await.push(format!("init:{}", self.name));
1834 Ok(())
1835 }
1836 }
1837
1838 #[async_trait::async_trait]
1839 impl SystemCapability for TrackHooks {
1840 fn pre_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1841 Ok(())
1842 }
1843
1844 async fn post_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1845 self.events
1846 .lock()
1847 .await
1848 .push(format!("post_init:{}", self.name));
1849 Ok(())
1850 }
1851 }
1852
1853 let events = Arc::new(Mutex::new(Vec::<String>::new()));
1854 let sys_a = Arc::new(TrackHooks {
1855 name: "sys_a",
1856 events: events.clone(),
1857 });
1858 let user_b = Arc::new(TrackHooks {
1859 name: "user_b",
1860 events: events.clone(),
1861 });
1862 let user_c = Arc::new(TrackHooks {
1863 name: "user_c",
1864 events: events.clone(),
1865 });
1866
1867 let mut builder = RegistryBuilder::default();
1868 builder.register_core_with_meta("sys_a", &[], sys_a.clone() as Arc<dyn Gear>);
1869 builder.register_core_with_meta("user_b", &["sys_a"], user_b.clone() as Arc<dyn Gear>);
1870 builder.register_core_with_meta("user_c", &["user_b"], user_c.clone() as Arc<dyn Gear>);
1871 builder.register_system_with_meta("sys_a", sys_a.clone() as Arc<dyn SystemCapability>);
1872
1873 let registry = builder.build_topo_sorted().unwrap();
1874
1875 let client_hub = Arc::new(ClientHub::new());
1876 let cancel = CancellationToken::new();
1877 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
1878
1879 let runtime = HostRuntime::new(
1880 registry,
1881 config_provider,
1882 DbOptions::None,
1883 client_hub,
1884 cancel,
1885 Uuid::new_v4(),
1886 None,
1887 );
1888
1889 runtime.run_init_phase().await.unwrap();
1891 runtime.run_post_init_phase().await.unwrap();
1892
1893 let events = events.lock().await.clone();
1894 let first_post_init = events
1895 .iter()
1896 .position(|e| e.starts_with("post_init:"))
1897 .expect("expected post_init events");
1898 assert!(
1899 events[..first_post_init]
1900 .iter()
1901 .all(|e| e.starts_with("init:")),
1902 "expected all init events before post_init, got: {events:?}"
1903 );
1904
1905 assert_eq!(
1907 events,
1908 vec![
1909 "init:sys_a",
1910 "init:user_b",
1911 "init:user_c",
1912 "post_init:sys_a",
1913 ]
1914 );
1915 }
1916
1917 #[tokio::test]
1931 async fn init_wiring_post_init_runs_as_one_ordered_segment() {
1932 #[derive(Clone)]
1933 struct TrackHooks {
1934 events: Arc<Mutex<Vec<String>>>,
1935 }
1936
1937 #[async_trait::async_trait]
1938 impl Gear for TrackHooks {
1939 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1940 self.events.lock().await.push("init".to_owned());
1941 Ok(())
1942 }
1943 }
1944
1945 #[async_trait::async_trait]
1946 impl SystemCapability for TrackHooks {
1947 fn pre_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1948 Ok(())
1949 }
1950
1951 async fn post_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
1952 self.events.lock().await.push("post_init".to_owned());
1953 Ok(())
1954 }
1955 }
1956
1957 let events = Arc::new(Mutex::new(Vec::<String>::new()));
1958 let gear = Arc::new(TrackHooks {
1959 events: events.clone(),
1960 });
1961
1962 let mut builder = RegistryBuilder::default();
1963 builder.register_core_with_meta("sys", &[], gear.clone() as Arc<dyn Gear>);
1964 builder.register_system_with_meta("sys", gear.clone() as Arc<dyn SystemCapability>);
1965 let registry = builder.build_topo_sorted().unwrap();
1966
1967 let runtime = HostRuntime::new(
1968 registry,
1969 Arc::new(EmptyConfigProvider) as Arc<dyn ConfigProvider>,
1970 DbOptions::None,
1971 Arc::new(ClientHub::new()),
1972 CancellationToken::new(),
1973 Uuid::new_v4(),
1974 None,
1975 );
1976
1977 runtime.run_init_wiring_post_init().await.unwrap();
1978
1979 assert_eq!(events.lock().await.clone(), vec!["init", "post_init"]);
1980 }
1981
1982 #[tokio::test]
1983 async fn test_stop_phase_provides_fresh_deadline_token() {
1984 use std::sync::atomic::AtomicBool;
1985
1986 struct TokenCheckGear {
1987 stop_was_called: AtomicBool,
1988 token_was_cancelled_on_entry: AtomicBool,
1989 }
1990
1991 #[async_trait::async_trait]
1992 impl Gear for TokenCheckGear {
1993 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
1994 Ok(())
1995 }
1996 }
1997
1998 #[async_trait::async_trait]
1999 impl RunnableCapability for TokenCheckGear {
2000 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
2001 Ok(())
2002 }
2003 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
2004 self.stop_was_called.store(true, Ordering::SeqCst);
2006 self.token_was_cancelled_on_entry
2008 .store(deadline_token.is_cancelled(), Ordering::SeqCst);
2009 Ok(())
2010 }
2011 }
2012
2013 let gear = Arc::new(TokenCheckGear {
2014 stop_was_called: AtomicBool::new(false),
2015 token_was_cancelled_on_entry: AtomicBool::new(true),
2017 });
2018
2019 let mut builder = RegistryBuilder::default();
2020 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
2021 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
2022
2023 let registry = builder.build_topo_sorted().unwrap();
2024 let client_hub = Arc::new(ClientHub::new());
2025 let cancel = CancellationToken::new();
2026 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
2027
2028 let runtime = HostRuntime::new(
2029 registry,
2030 config_provider,
2031 DbOptions::None,
2032 client_hub,
2033 cancel.clone(),
2034 Uuid::new_v4(),
2035 None,
2036 );
2037
2038 runtime.run_stop_phase().await.unwrap();
2040
2041 assert!(
2043 gear.stop_was_called.load(Ordering::SeqCst),
2044 "stop() was never called - gear may not have been registered correctly"
2045 );
2046
2047 assert!(
2050 !gear.token_was_cancelled_on_entry.load(Ordering::SeqCst),
2051 "deadline_token should NOT be cancelled when stop() is called - this enables graceful shutdown"
2052 );
2053 }
2054
2055 #[tokio::test]
2056 async fn test_stop_phase_graceful_shutdown_completes_before_deadline() {
2057 use std::sync::atomic::AtomicBool;
2058 use std::time::Duration;
2059
2060 struct GracefulGear {
2061 graceful_completed: AtomicBool,
2062 deadline_fired: AtomicBool,
2063 }
2064
2065 #[async_trait::async_trait]
2066 impl Gear for GracefulGear {
2067 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
2068 Ok(())
2069 }
2070 }
2071
2072 #[async_trait::async_trait]
2073 impl RunnableCapability for GracefulGear {
2074 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
2075 Ok(())
2076 }
2077 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
2078 tokio::select! {
2080 () = tokio::time::sleep(Duration::from_millis(10)) => {
2081 self.graceful_completed.store(true, Ordering::SeqCst);
2082 }
2083 () = deadline_token.cancelled() => {
2084 self.deadline_fired.store(true, Ordering::SeqCst);
2085 }
2086 }
2087 Ok(())
2088 }
2089 }
2090
2091 let gear = Arc::new(GracefulGear {
2092 graceful_completed: AtomicBool::new(false),
2093 deadline_fired: AtomicBool::new(false),
2094 });
2095
2096 let mut builder = RegistryBuilder::default();
2097 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
2098 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
2099
2100 let registry = builder.build_topo_sorted().unwrap();
2101 let client_hub = Arc::new(ClientHub::new());
2102 let cancel = CancellationToken::new();
2103 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
2104
2105 let runtime = HostRuntime::new(
2107 registry,
2108 config_provider,
2109 DbOptions::None,
2110 client_hub,
2111 cancel.clone(),
2112 Uuid::new_v4(),
2113 None,
2114 )
2115 .with_shutdown_deadline(Duration::from_secs(5));
2116
2117 runtime.run_stop_phase().await.unwrap();
2118
2119 assert!(
2121 gear.graceful_completed.load(Ordering::SeqCst),
2122 "graceful shutdown should complete"
2123 );
2124 assert!(
2126 !gear.deadline_fired.load(Ordering::SeqCst),
2127 "deadline should not fire when graceful shutdown completes quickly"
2128 );
2129 }
2130
2131 #[tokio::test]
2132 async fn test_stop_phase_deadline_fires_for_slow_gear() {
2133 use std::sync::atomic::AtomicBool;
2134 use std::time::Duration;
2135
2136 struct SlowGear {
2137 graceful_completed: AtomicBool,
2138 deadline_fired: AtomicBool,
2139 }
2140
2141 #[async_trait::async_trait]
2142 impl Gear for SlowGear {
2143 async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
2144 Ok(())
2145 }
2146 }
2147
2148 #[async_trait::async_trait]
2149 impl RunnableCapability for SlowGear {
2150 async fn start(&self, _cancel: CancellationToken) -> anyhow::Result<()> {
2151 Ok(())
2152 }
2153 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
2154 tokio::select! {
2156 () = tokio::time::sleep(Duration::from_secs(10)) => {
2157 self.graceful_completed.store(true, Ordering::SeqCst);
2158 }
2159 () = deadline_token.cancelled() => {
2160 self.deadline_fired.store(true, Ordering::SeqCst);
2161 }
2162 }
2163 Ok(())
2164 }
2165 }
2166
2167 let gear = Arc::new(SlowGear {
2168 graceful_completed: AtomicBool::new(false),
2169 deadline_fired: AtomicBool::new(false),
2170 });
2171
2172 let mut builder = RegistryBuilder::default();
2173 builder.register_core_with_meta("test", &[], gear.clone() as Arc<dyn Gear>);
2174 builder.register_stateful_with_meta("test", gear.clone() as Arc<dyn RunnableCapability>);
2175
2176 let registry = builder.build_topo_sorted().unwrap();
2177 let client_hub = Arc::new(ClientHub::new());
2178 let cancel = CancellationToken::new();
2179 let config_provider: Arc<dyn ConfigProvider> = Arc::new(EmptyConfigProvider);
2180
2181 let runtime = HostRuntime::new(
2183 registry,
2184 config_provider,
2185 DbOptions::None,
2186 client_hub,
2187 cancel.clone(),
2188 Uuid::new_v4(),
2189 None,
2190 )
2191 .with_shutdown_deadline(Duration::from_millis(100));
2192
2193 runtime.run_stop_phase().await.unwrap();
2194
2195 assert!(
2197 !gear.graceful_completed.load(Ordering::SeqCst),
2198 "graceful shutdown should not complete when deadline fires first"
2199 );
2200 assert!(
2202 gear.deadline_fired.load(Ordering::SeqCst),
2203 "deadline should fire for slow gears"
2204 );
2205 }
2206}