Skip to main content

toolkit/runtime/
host_runtime.rs

1//! Host Runtime - orchestrates the full `ToolKit` lifecycle
2//!
3//! This gear contains the `HostRuntime` type that owns and coordinates
4//! the execution of all lifecycle phases.
5//!
6//! High-level phase order:
7//! - `pre_init` (system gears only)
8//! - DB migrations (gears with DB capability)
9//! - `init` (all gears)
10//! - `post_init` (system gears only; runs after *all* `init` complete)
11//! - REST wiring (gears with REST capability; requires a single REST host)
12//! - gRPC registration (gears with gRPC capability; requires a single gRPC hub)
13//! - start/stop (stateful gears)
14//! - `OoP` spawn / wait / stop (host-only orchestration)
15
16use 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/// How the runtime should provide DBs to gears.
37#[derive(Clone)]
38pub enum DbOptions {
39    /// No database integration. `GearCtx::db()` will be `None`, `db_required()` will error.
40    None,
41    /// Use a `DbManager` to handle database connections with Figment-based configuration.
42    #[cfg(feature = "db")]
43    Manager(Arc<toolkit_db::DbManager>),
44}
45
46/// Runtime execution mode that determines which phases to run.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum RunMode {
49    /// Run all phases and wait for shutdown signal (normal application mode).
50    Full,
51    /// Run only pre-init and DB migration phases, then exit (for cloud deployments).
52    MigrateOnly,
53}
54
55/// Environment variable name for passing directory endpoint to `OoP` gears.
56pub const TOOLKIT_DIRECTORY_ENDPOINT_ENV: &str = "TOOLKIT_DIRECTORY_ENDPOINT";
57
58/// Environment variable name for passing rendered gear config to `OoP` gears.
59pub const TOOLKIT_MODULE_CONFIG_ENV: &str = "TOOLKIT_MODULE_CONFIG";
60
61/// Default shutdown deadline for graceful gear stop (35 seconds).
62///
63/// This is intentionally 5 seconds longer than `WithLifecycle::stop_timeout` (30s default)
64/// to ensure deterministic behavior: the lifecycle's internal timeout fires first,
65/// and the runtime deadline acts as a hard backstop.
66pub const DEFAULT_SHUTDOWN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(35);
67
68/// `HostRuntime` owns the lifecycle orchestration for `ToolKit`.
69///
70/// It encapsulates all runtime state and drives gears through the full lifecycle (see gear docs).
71pub 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` gear spawn configuration and backend
83    oop_options: Option<OopSpawnOptions>,
84    /// Maximum time allowed for graceful shutdown before hard-stop signal is sent.
85    shutdown_deadline: std::time::Duration,
86}
87
88impl HostRuntime {
89    /// Create a new `HostRuntime` instance.
90    ///
91    /// This prepares all runtime components but does not start any lifecycle phases.
92    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        // Create runtime-owned components for system gears
102        let gear_manager = Arc::new(GearManager::new());
103        let grpc_installers = Arc::new(GrpcInstallerStore::new());
104
105        // Build the context builder that will resolve per-gear DbHandles
106        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    /// Set a custom shutdown deadline for graceful gear stop.
129    ///
130    /// This is the maximum time the runtime will wait for each gear to stop gracefully
131    /// before sending the hard-stop signal (cancelling the deadline token).
132    ///
133    /// # Relationship with `WithLifecycle::stop_timeout`
134    ///
135    /// When using `WithLifecycle`, its `stop_timeout` (default 30s) races against this
136    /// `shutdown_deadline` (also default 30s). To ensure deterministic behavior:
137    ///
138    /// - `WithLifecycle::stop_timeout` should be **less than** `shutdown_deadline`
139    /// - This allows the lifecycle's internal timeout to trigger first for graceful cleanup
140    /// - The runtime's `deadline_token` then acts as a hard backstop
141    ///
142    /// Example: `stop_timeout = 25s`, `shutdown_deadline = 30s`
143    #[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    /// `PRE_INIT` phase: wire runtime internals into system gears.
150    ///
151    /// This phase runs before init and only for gears with the "system" capability.
152    ///
153    /// # Errors
154    /// Returns `RegistryError` if system wiring fails.
155    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            // Check for cancellation before processing each gear
166            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    /// Helper: resolve context for a gear with error mapping.
186    #[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    /// Helper: extract DB handle and gear if both exist.
201    #[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        // Important: DB migrations require access to the underlying `Db`, not just `DBProvider`.
219        // `GearCtx` intentionally exposes only `DBProvider` for better DX and to reduce mistakes.
220        // So the runtime resolves the `Db` directly from its `DbManager`.
221        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; // ctx is kept for parity/error context; DB is resolved from manager above.
235        Ok(db.map(|db| (db, dbm)))
236    }
237
238    /// Helper: run migrations for a single gear using the new migration runner.
239    ///
240    /// This collects migrations from the gear and executes them via the
241    /// runtime's privileged connection. Gears never see the raw connection.
242    #[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        // Collect migrations from the gear
249        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        // Execute migrations using the migration runner
263        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    /// DB MIGRATION phase: run migrations for all gears with DB capability.
282    ///
283    /// Runs before init, with system gears processed first.
284    ///
285    /// Gears provide migrations via `DatabaseCapability::migrations()`.
286    /// The runtime executes them with a privileged connection that gears
287    /// never receive directly. Each gear gets a separate migration history
288    /// table, preventing cross-gear interference.
289    #[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            // Check for cancellation before processing each gear
295            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    /// INIT phase: initialize all gears in topological order.
324    ///
325    /// System gears initialize first, followed by user gears.
326    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    /// `POST_INIT` phase: optional hook after ALL gears completed `init()`.
354    ///
355    /// This provides a global barrier between initialization-time registration
356    /// and subsequent phases that may rely on a fully-populated runtime registry.
357    ///
358    /// System gears run first, followed by user gears, preserving topo order.
359    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    /// REST phase: compose the router against the REST host.
384    ///
385    /// This is a synchronous phase that builds the final Router by:
386    /// 1. Preparing the host gear
387    /// 2. Registering all REST providers
388    /// 3. Finalizing with `OpenAPI` endpoints
389    async fn run_rest_phase(&self) -> Result<Router, RegistryError> {
390        tracing::info!("Phase: rest (sync)");
391
392        let mut router = Router::new();
393
394        // Find host(s) and whether any rest gears exist
395        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 => { /* proceed */ }
416            _ => return Err(RegistryError::MultipleRestHosts),
417        }
418
419        // Resolve the single host entry and its gear context
420        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        // use host as the registry
440        let registry: &dyn crate::contracts::OpenApiRegistry = host.as_registry();
441
442        // Healthcheck registry, passed explicitly to the REST host and providers below
443        // (not via ClientHub). Seeded with the host's shutdown token so in-flight checks
444        // are aborted on shutdown.
445        let hc_registry = Arc::new(
446            crate::healthcheck::RestHealthcheckRegistry::with_cancellation(
447                host_ctx.cancellation_token().clone(),
448            ),
449        );
450
451        // 1) Host prepare: base Router / global middlewares / basic OAS meta
452        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        // 2) Register all REST providers (in the current discovery order)
460        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                // Register the gear's readiness healthcheck after successful route registration.
477                if let Some(hc) = rest.healthcheck(&ctx) {
478                    hc_registry.register(e.name, hc);
479                }
480            }
481        }
482
483        // 3) Host finalize: attach /openapi.json and /docs, persist Router if needed (no server start)
484        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    /// gRPC registration phase: collect services from all grpc gears.
495    ///
496    /// Services are stored in the installer store for the `grpc-hub` to consume during start.
497    async fn run_grpc_phase(&self) -> Result<(), RegistryError> {
498        tracing::info!("Phase: grpc (registration)");
499
500        // If no grpc_hub and no grpc_services, skip the phase
501        if self.registry.grpc_hub.is_none() && self.registry.grpc_services.is_empty() {
502            return Ok(());
503        }
504
505        // If there are grpc_services but no hub, that's an error
506        if self.registry.grpc_hub.is_none() && !self.registry.grpc_services.is_empty() {
507            return Err(RegistryError::GrpcRequiresHub);
508        }
509
510        // If there's a hub, collect all services grouped by gear and hand them off to the installer store
511        if let Some(hub_name) = &self.registry.grpc_hub {
512            let mut gears_data = Vec::new();
513            let mut seen = HashSet::new();
514
515            // Collect services from all grpc gears
516            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    /// START phase: start all stateful gears.
562    ///
563    /// System gears start first, followed by user gears.
564    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    /// Stop a single gear, logging errors but continuing execution.
588    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    /// STOP phase: stop all stateful gears in reverse order.
602    ///
603    /// # Two-Phase Shutdown Contract
604    ///
605    /// This phase implements a proper two-phase shutdown for **each gear**:
606    ///
607    /// 1. **Graceful stop request**: Each gear's `stop(deadline_token)` is called with a
608    ///    *fresh* cancellation token (not the already-cancelled root token). Gears should
609    ///    interpret this as "please stop gracefully".
610    ///
611    /// 2. **Hard-stop deadline**: After `shutdown_deadline` expires **for that gear**,
612    ///    its `deadline_token` is cancelled. Gears should interpret this as "abort immediately".
613    ///
614    /// Each gear gets its own independent deadline — if gear A takes 25s to stop,
615    /// gear B still gets the full `shutdown_deadline` for its graceful shutdown.
616    ///
617    /// This allows gears to implement real graceful shutdown:
618    /// - Request cooperative shutdown of child tasks
619    /// - Wait for them to finish gracefully
620    /// - If `deadline_token` fires, switch to hard-abort mode
621    ///
622    /// Errors are logged but do not fail the shutdown process.
623    /// Note: `OoP` gears are stopped automatically by the backend when the
624    /// cancellation token is triggered.
625    async fn run_stop_phase(&self) -> Result<(), RegistryError> {
626        tracing::info!("Phase: stop");
627
628        let deadline = self.shutdown_deadline;
629
630        // Stop all gears in reverse order, each with its own independent deadline
631        for e in self.registry.gears().iter().rev() {
632            let gear_name = e.name;
633
634            // Create a fresh deadline token for THIS gear
635            // Each gear gets the full shutdown_deadline independently
636            let deadline_token = CancellationToken::new();
637            let deadline_token_for_timeout = deadline_token.clone();
638
639            // Spawn a task to cancel this gear's deadline token after shutdown_deadline
640            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            // Stop this gear with its own deadline token
651            // The gear can observe the token transition from uncancelled→cancelled
652            Self::stop_one_gear(e, deadline_token).await;
653
654            // Cancel the deadline task and await it to ensure full cleanup
655            deadline_task.abort();
656            #[allow(clippy::let_underscore_must_use)]
657            let _ = deadline_task.await;
658        }
659
660        Ok(())
661    }
662
663    /// `OoP` SPAWN phase: spawn out-of-process gears after start phase.
664    ///
665    /// This phase runs after `grpc-hub` is already listening, so we can pass
666    /// the real directory endpoint to `OoP` gears.
667    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        // Wait for grpc_hub to publish its endpoint (it runs async in start phase)
676        let directory_endpoint = self.wait_for_grpc_hub_endpoint().await;
677
678        for gear_cfg in &oop_opts.gears {
679            // Build environment with directory endpoint and rendered config
680            // Note: User controls --config via execution.args in master config
681            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            // Use args from execution config as-is (user controls --config via args)
691            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    /// Wait for `grpc-hub` to publish its bound endpoint.
721    ///
722    /// Polls the `GrpcHubGear::bound_endpoint()` with a short interval until available or timeout.
723    /// Returns None if no `grpc-hub` is running or if it times out.
724    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        // Find grpc_hub in registry
729        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; // No grpc_hub registered
737        };
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    /// Run the full gear lifecycle (all phases).
761    ///
762    /// This is the standard entry point for normal application execution.
763    /// It runs all phases from pre-init through shutdown.
764    ///
765    /// # Errors
766    ///
767    /// Returns an error if any gear phase fails during execution.
768    pub async fn run_gear_phases(self) -> anyhow::Result<()> {
769        self.run_phases_internal(RunMode::Full).await
770    }
771
772    /// Run only the migration phases (pre-init + DB migration).
773    ///
774    /// This is designed for cloud deployment workflows where database migrations
775    /// need to run as a separate step before starting the application.
776    /// The process exits after migrations complete.
777    ///
778    /// # Errors
779    ///
780    /// Returns an error if pre-init or migration phases fail.
781    pub async fn run_migration_phases(self) -> anyhow::Result<()> {
782        self.run_phases_internal(RunMode::MigrateOnly).await
783    }
784
785    /// Internal implementation that runs gear phases based on the mode.
786    ///
787    /// This private method contains the actual phase execution logic and is called
788    /// by both `run_gear_phases()` and `run_migration_phases()`.
789    ///
790    /// # Modes
791    ///
792    /// - `RunMode::Full`: Executes all phases and waits for shutdown signal
793    /// - `RunMode::MigrateOnly`: Executes only pre-init and DB migration phases, then exits
794    ///
795    /// # Phases (Full Mode)
796    ///
797    /// 1. Pre-init (system gears only)
798    /// 2. DB migration (all gears with database capability)
799    /// 3. Init (all gears)
800    /// 4. Post-init (system gears only)
801    /// 5. REST (gears with REST capability)
802    /// 6. gRPC (gears with gRPC capability)
803    /// 7. Start (runnable gears)
804    /// 8. `OoP` spawn (out-of-process gears)
805    /// 9. Wait for cancellation
806    /// 10. Stop (runnable gears in reverse order)
807    async fn run_phases_internal(self, mode: RunMode) -> anyhow::Result<()> {
808        // Log execution mode
809        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        // 1. Pre-init phase (before init, only for system gears)
819        self.run_pre_init_phase()?;
820
821        // 2. DB migration phase (system gears first)
822        #[cfg(feature = "db")]
823        {
824            self.run_db_phase().await?;
825        }
826        #[cfg(not(feature = "db"))]
827        {
828            // No DB integration in this build.
829        }
830
831        // Exit early if running in migration-only mode
832        if mode == RunMode::MigrateOnly {
833            tracing::info!("Migration phases completed successfully");
834            return Ok(());
835        }
836
837        // 3. Init phase (system gears first)
838        self.run_init_phase().await?;
839
840        // 4. Post-init phase (barrier after ALL init; system gears only)
841        self.run_post_init_phase().await?;
842
843        // 5. REST phase (synchronous router composition)
844        let _router = self.run_rest_phase().await?;
845
846        // 6. gRPC registration phase
847        self.run_grpc_phase().await?;
848
849        // 7. Start phase
850        self.run_start_phase().await?;
851
852        // 8. OoP spawn phase (after grpc_hub is running)
853        self.run_oop_spawn_phase().await?;
854
855        // 9. Wait for cancellation
856        self.cancel.cancelled().await;
857
858        // 10. Stop phase with hard timeout.
859        //     Blocking syscalls (e.g. libc getaddrinfo in tokio spawn_blocking)
860        //     can saturate all tokio worker threads, preventing tokio timers
861        //     from firing. Use an OS thread so the watchdog works even when
862        //     the tokio runtime is fully blocked.
863        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        // Verify gear order is a -> b -> c
960        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        // Run stop phase
978        runtime.run_stop_phase().await.unwrap();
979
980        // Verify gears stopped in reverse order: c (stop_order=0), b (stop_order=1), a (stop_order=2)
981        // Gear order is: a=0, b=1, c=2
982        // Stop order should be: c=0, b=1, a=2
983        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        // Run stop phase - should not fail even though gear_b fails
1054        runtime.run_stop_phase().await.unwrap();
1055
1056        // All gears should have attempted to stop
1057        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        // Run init phase for all gears, then post_init as a separate barrier phase.
1135        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        // system-first order within each phase
1151        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                // Record that stop() was called
1185                self.stop_was_called.store(true, Ordering::SeqCst);
1186                // Record whether the token was already cancelled when stop() was called
1187                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            // Default to true to detect if stop() was never called
1196            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        // Run stop phase - the deadline token should NOT be cancelled
1219        runtime.run_stop_phase().await.unwrap();
1220
1221        // First, verify stop() was actually called (guards against silent registration failures)
1222        assert!(
1223            gear.stop_was_called.load(Ordering::SeqCst),
1224            "stop() was never called - gear may not have been registered correctly"
1225        );
1226
1227        // The token should NOT have been cancelled when stop() was called
1228        // This is the key fix: gears get a fresh token, not the already-cancelled root token
1229        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                // Simulate graceful shutdown that completes quickly (10ms)
1259                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        // Use a long deadline (5s) - gear should complete gracefully before this
1286        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        // Graceful shutdown should have completed
1300        assert!(
1301            gear.graceful_completed.load(Ordering::SeqCst),
1302            "graceful shutdown should complete"
1303        );
1304        // Deadline should NOT have fired (gear finished before deadline)
1305        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                // Simulate slow graceful shutdown (would take 10s, but deadline is 100ms)
1335                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        // Use a short deadline (100ms) - gear should be interrupted by deadline
1362        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        // Graceful shutdown should NOT have completed (deadline fired first)
1376        assert!(
1377            !gear.graceful_completed.load(Ordering::SeqCst),
1378            "graceful shutdown should not complete when deadline fires first"
1379        );
1380        // Deadline should have fired
1381        assert!(
1382            gear.deadline_fired.load(Ordering::SeqCst),
1383            "deadline should fire for slow gears"
1384        );
1385    }
1386}