Skip to main content

modkit/runtime/
runner.rs

1//! `ModKit` runtime runner.
2//!
3//! Supported DB modes:
4//!   - `DbOptions::None` — modules get no DB in their contexts.
5//!   - `DbOptions::Manager` — modules use `ModuleContextBuilder` to resolve per-module `DbHandles`.
6//!
7//! Design notes:
8//! - We use **`ModuleContextBuilder`** to resolve per-module `DbHandles` at runtime.
9//! - Phase order is orchestrated by `HostRuntime` (see `runtime/host_runtime.rs` docs).
10//! - Modules receive a fully-scoped `ModuleCtx` with a resolved Option<DbHandle>.
11//! - Shutdown can be driven by OS signals, an external `CancellationToken`,
12//!   or an arbitrary future.
13//! - Pre-registered clients can be injected into the `ClientHub` via `RunOptions::clients`.
14//! - `OoP` modules are spawned after the start phase so that `grpc-hub` is already running
15//!   and the real directory endpoint is known.
16
17use crate::backends::OopBackend;
18use crate::client_hub::ClientHub;
19use crate::config::ConfigProvider;
20use crate::registry::ModuleRegistry;
21use crate::runtime::shutdown;
22use crate::runtime::{DbOptions, HostRuntime};
23use std::collections::HashMap;
24use std::path::PathBuf;
25use std::{future::Future, pin::Pin, sync::Arc};
26use tokio_util::sync::CancellationToken;
27use uuid::Uuid;
28
29/// A type-erased client registration for injecting clients into the `ClientHub`.
30///
31/// This is used to pass pre-created clients (like gRPC clients) from bootstrap code
32/// into the runtime's `ClientHub` before modules are initialized.
33pub struct ClientRegistration {
34    /// Callback that registers the client into the hub.
35    register_fn: Box<dyn FnOnce(&ClientHub) + Send>,
36}
37
38impl ClientRegistration {
39    /// Create a new client registration for a trait object type.
40    ///
41    /// # Example
42    /// ```ignore
43    /// let api: Arc<dyn DirectoryClient> = Arc::new(client);
44    /// ClientRegistration::new::<dyn DirectoryClient>(api)
45    /// ```
46    pub fn new<T>(client: Arc<T>) -> Self
47    where
48        T: ?Sized + Send + Sync + 'static,
49    {
50        Self {
51            register_fn: Box::new(move |hub| {
52                hub.register::<T>(client);
53            }),
54        }
55    }
56
57    /// Execute the registration against the given hub.
58    pub(crate) fn apply(self, hub: &ClientHub) {
59        (self.register_fn)(hub);
60    }
61}
62
63/// How the runtime should decide when to stop.
64pub enum ShutdownOptions {
65    /// Listen for OS signals (Ctrl+C / SIGTERM).
66    Signals,
67    /// An external `CancellationToken` controls the lifecycle.
68    Token(CancellationToken),
69    /// An arbitrary future; when it completes, we initiate shutdown.
70    Future(Pin<Box<dyn Future<Output = ()> + Send>>),
71}
72
73/// Configuration for a single `OoP` module to be spawned.
74#[derive(Clone)]
75pub struct OopModuleSpawnConfig {
76    /// Module name (e.g., "calculator")
77    pub module_name: String,
78    /// Path to the executable
79    pub binary: PathBuf,
80    /// Command-line arguments (user controls --config via execution.args in master config)
81    pub args: Vec<String>,
82    /// Environment variables to set
83    pub env: HashMap<String, String>,
84    /// Working directory for the process
85    pub working_directory: Option<String>,
86    /// Rendered module config JSON (for `MODKIT_MODULE_CONFIG` env var)
87    pub rendered_config_json: String,
88}
89
90/// Options for spawning `OoP` modules.
91pub struct OopSpawnOptions {
92    /// List of `OoP` modules to spawn after the start phase
93    pub modules: Vec<OopModuleSpawnConfig>,
94    /// Backend for spawning `OoP` modules (e.g., `LocalProcessBackend`)
95    pub backend: Box<dyn OopBackend>,
96}
97
98/// Options for running the `ModKit` runner.
99pub struct RunOptions {
100    /// Provider of module config sections (raw JSON by module name).
101    pub modules_cfg: Arc<dyn ConfigProvider>,
102    /// DB strategy: none, or `DbManager`.
103    pub db: DbOptions,
104    /// Shutdown strategy.
105    pub shutdown: ShutdownOptions,
106    /// Pre-registered clients to inject into the `ClientHub` before module initialization.
107    ///
108    /// This is useful for `OoP` bootstrap where clients (like `DirectoryGrpcClient`)
109    /// are created before calling `run()` and need to be available in the `ClientHub`.
110    pub clients: Vec<ClientRegistration>,
111    /// Process-level instance ID.
112    ///
113    /// This is a unique identifier for this process instance, generated once at bootstrap
114    /// (either in `run_oop_with_options` for `OoP` modules or in the main host).
115    /// It is propagated to all modules via `ModuleCtx::instance_id()` and `SystemContext::instance_id()`.
116    pub instance_id: Uuid,
117    /// `OoP` module spawn configuration.
118    ///
119    /// These modules are spawned after the start phase, once `grpc-hub` is running
120    /// and the real directory endpoint is known.
121    pub oop: Option<OopSpawnOptions>,
122}
123
124/// Full cycle is orchestrated by `HostRuntime` (see `runtime/host_runtime.rs` docs).
125///
126/// This function is a thin wrapper around `HostRuntime` that handles shutdown signal setup
127/// and then delegates all lifecycle orchestration to the `HostRuntime`.
128///
129/// # Errors
130/// Returns an error if any lifecycle phase fails.
131pub async fn run(opts: RunOptions) -> anyhow::Result<()> {
132    // 1. Prepare cancellation token based on shutdown options
133    let cancel = match &opts.shutdown {
134        ShutdownOptions::Token(t) => t.clone(),
135        _ => CancellationToken::new(),
136    };
137
138    // 2. Spawn shutdown waiter (Signals / Future) just like before
139    match opts.shutdown {
140        ShutdownOptions::Signals => {
141            let c = cancel.clone();
142            tokio::spawn(async move {
143                match shutdown::wait_for_shutdown().await {
144                    Ok(()) => {
145                        tracing::info!(target: "", "------------------");
146                        tracing::info!("shutdown: signal received");
147                    }
148                    Err(e) => {
149                        tracing::warn!(
150                            error = %e,
151                            "shutdown: primary waiter failed; falling back to ctrl_c()"
152                        );
153                        _ = tokio::signal::ctrl_c().await;
154                    }
155                }
156                c.cancel();
157            });
158        }
159        ShutdownOptions::Future(waiter) => {
160            let c = cancel.clone();
161            tokio::spawn(async move {
162                waiter.await;
163                tracing::info!("shutdown: external future completed");
164                c.cancel();
165            });
166        }
167        ShutdownOptions::Token(_) => {
168            tracing::info!("shutdown: external token will control lifecycle");
169        }
170    }
171
172    // 3. Discover modules
173    let registry = ModuleRegistry::discover_and_build()?;
174
175    // 4. Build shared ClientHub
176    let hub = Arc::new(ClientHub::default());
177
178    // 4b. Apply pre-registered clients from RunOptions
179    for registration in opts.clients {
180        registration.apply(&hub);
181    }
182
183    // 5. Instantiate HostRuntime
184    let host = HostRuntime::new(
185        registry,
186        opts.modules_cfg.clone(),
187        opts.db,
188        hub,
189        cancel.clone(),
190        opts.instance_id,
191        opts.oop,
192    );
193
194    // 6. Run full lifecycle
195    host.run_module_phases().await
196}