diff --git a/crates/nexum-cli/src/launch.rs b/crates/nexum-cli/src/launch.rs index 52829cc5..99db2562 100644 --- a/crates/nexum-cli/src/launch.rs +++ b/crates/nexum-cli/src/launch.rs @@ -4,7 +4,7 @@ use std::path::Path; -use nexum_runtime::addons::{PrometheusAddOn, RuntimeAddOns}; +use nexum_runtime::addons::{PrometheusAddOn, RuntimeAddOn}; use nexum_runtime::builder::RuntimeBuilder; use nexum_runtime::engine_config::EngineConfig; use nexum_runtime::host::component::{ @@ -12,6 +12,7 @@ use nexum_runtime::host::component::{ }; use nexum_runtime::host::local_store_redb::LocalStore; use nexum_runtime::host::provider_pool::ProviderPool; +use nexum_runtime::runtime::task::TokioExecutor; use shepherd_cow_host::{ReferenceExt, ReferenceExtBuilder, extension}; /// The backends the reference engine ships: the core seams plus the @@ -40,7 +41,7 @@ pub async fn run_from_config( // Attach the reference add-on set. The binary ships the Prometheus // exporter; an embedder omits or replaces it by choosing a different // list here. - let add_ons: [&dyn RuntimeAddOns; 1] = [&PrometheusAddOn]; + let add_ons: [&dyn RuntimeAddOn; 1] = [&PrometheusAddOn]; // Assemble and launch over the type-state builder: bind the reference // lattice, wire cow-api as an extension (linker hook plus capability @@ -51,6 +52,10 @@ pub async fn run_from_config( .with_types::() .with_extensions([extension::()]) .with_module_source(wasm.map(Path::to_path_buf), manifest.map(Path::to_path_buf)) + // The launch root is the executor-selection seam: the binary spawns on + // tokio, while an embedder or a non-tokio target substitutes its own + // executor here. + .with_executor(&TokioExecutor) .with_components(ComponentsBuilder::new( ProviderPoolBuilder, LocalStoreBuilder, diff --git a/crates/nexum-runtime/src/addons.rs b/crates/nexum-runtime/src/addons.rs index 4709adfc..114d2357 100644 --- a/crates/nexum-runtime/src/addons.rs +++ b/crates/nexum-runtime/src/addons.rs @@ -7,7 +7,7 @@ //! replaces any of them instead of inheriting a fixed install. //! //! A future control-surface add-on (an admin or RPC socket) slots in beside -//! [`PrometheusAddOn`]: implement [`RuntimeAddOns`], read its own section +//! [`PrometheusAddOn`]: implement [`RuntimeAddOn`], read its own section //! from [`AddOnsContext`], and add it to the launcher's list at the //! composition root. @@ -39,7 +39,7 @@ impl AddOnHandle { /// A process-wide facility attached to the launch path. `install` reads the /// resolved config from `ctx` and returns a handle the launcher retains. -pub trait RuntimeAddOns { +pub trait RuntimeAddOn { /// Install the facility, returning its live handle. fn install(&self, ctx: &AddOnsContext<'_>) -> anyhow::Result; } @@ -47,7 +47,7 @@ pub trait RuntimeAddOns { /// An owned, ordered add-on set gathered behind one value. A preset or /// composition root returns this so a heterogeneous set travels together; /// the launcher borrows each element to install it. -pub type AddOns = Vec>; +pub type AddOns = Vec>; /// The Prometheus exporter add-on. With `[engine.metrics].enabled = true` /// it binds an HTTP listener serving `/metrics`; otherwise it installs the @@ -56,7 +56,7 @@ pub type AddOns = Vec>; /// production with observability by flipping one config flag. pub struct PrometheusAddOn; -impl RuntimeAddOns for PrometheusAddOn { +impl RuntimeAddOn for PrometheusAddOn { fn install(&self, ctx: &AddOnsContext<'_>) -> anyhow::Result { if ctx.metrics.enabled { let addr: std::net::SocketAddr = ctx.metrics.bind_addr.parse().map_err(|e| { diff --git a/crates/nexum-runtime/src/bootstrap.rs b/crates/nexum-runtime/src/bootstrap.rs index a2b202c0..fca4b2d6 100644 --- a/crates/nexum-runtime/src/bootstrap.rs +++ b/crates/nexum-runtime/src/bootstrap.rs @@ -11,7 +11,7 @@ use std::path::Path; -use crate::addons::RuntimeAddOns; +use crate::addons::RuntimeAddOn; use crate::builder::{AssembledRuntime, LaunchContext, LaunchRuntime}; use crate::engine_config::EngineConfig; use crate::host::component::{Components, RuntimeTypes}; @@ -34,7 +34,7 @@ pub async fn run( manifest: Option<&Path>, components: &Components, extensions: &[Extension], - add_ons: &[&dyn RuntimeAddOns], + add_ons: &[&dyn RuntimeAddOn], ) -> anyhow::Result<()> { let runtime = AssembledRuntime { components: components.clone(), @@ -47,7 +47,6 @@ pub async fn run( let executor = TokioExecutor; let ctx = LaunchContext { executor: &executor, - data_dir: &engine_cfg.engine.state_dir, config: engine_cfg, }; runtime.launch(ctx).await?.wait().await diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs index aa1effe6..86457759 100644 --- a/crates/nexum-runtime/src/builder.rs +++ b/crates/nexum-runtime/src/builder.rs @@ -22,7 +22,7 @@ use std::path::{Path, PathBuf}; use tracing::{info, warn}; use wasmtime::Engine; -use crate::addons::{AddOnHandle, AddOnsContext, RuntimeAddOns}; +use crate::addons::{AddOnHandle, AddOnsContext, RuntimeAddOn}; use crate::engine_config::EngineConfig; use crate::host::component::{ BuilderContext, ComponentBuilder, Components, ComponentsBuilder, RuntimeTypes, @@ -36,16 +36,10 @@ pub use crate::supervisor::WasiClockOverride; use crate::supervisor::{self, Supervisor}; /// Ambient inputs the imperative launcher reads: the executor that spawns the -/// long-lived subscription and event-loop tasks, the resolved data directory, -/// and the loaded config. +/// long-lived subscription and event-loop tasks, and the loaded config. pub struct LaunchContext<'a> { /// Spawns the subscription and event-loop tasks. pub executor: &'a dyn TaskExecutor, - /// Directory the backends root their on-disk state at. Advisory: the - /// launcher receives pre-built backends, so it does not open the data - /// directory itself; a builder that opens the backends reads the data - /// directory at build time, not here. - pub data_dir: &'a Path, /// The loaded engine config. pub config: &'a EngineConfig, } @@ -98,7 +92,7 @@ pub struct AssembledRuntime<'a, T: RuntimeTypes> { /// Linker hooks and capability namespaces. pub extensions: Vec>, /// Cross-cutting facilities installed before the engine boots. - pub add_ons: &'a [&'a dyn RuntimeAddOns], + pub add_ons: &'a [&'a dyn RuntimeAddOn], /// Single-module source override; `None` runs `[[modules]]`. pub wasm: Option<&'a Path>, /// Manifest paired with `wasm`. @@ -231,8 +225,8 @@ impl LaunchRuntime for AssembledRuntime<'_, T> { let event_loop = ctx.executor.spawn(Box::pin(async move { let shutdown = async move { // A failed signal registration must not resolve the shutdown - // future: park that leg so the programmatic trigger (or the - // handle dropping) remains the only stop. + // future; hold this leg on `pending()` so the programmatic + // trigger (or the handle dropping) stays the only stop. let signal = async { match event_loop::wait_for_shutdown_signal().await { Ok(name) => info!(signal = %name, "shutdown signal received"), @@ -247,7 +241,7 @@ impl LaunchRuntime for AssembledRuntime<'_, T> { () = signal => {}, } }; - let mut supervisor = supervisor; + let mut supervisor = supervisor; // rebind as mut: the dispatch calls below take &mut self event_loop::run( &mut supervisor, block_streams, @@ -341,8 +335,8 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> { self } - /// Bind the executor the launcher spawns its tasks on. Defaults to the - /// ambient tokio runtime. + /// Bind the executor the launcher spawns its tasks on. Defaults to + /// [`TokioExecutor`], which spawns on the ambient tokio runtime. pub fn with_executor(mut self, executor: &'a dyn TaskExecutor) -> Self { self.executor = Some(executor); self @@ -358,8 +352,8 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> { /// Open the preset's backends and launch. Builds the [`Components`] bundle /// from the preset's component builders, installs the preset's add-ons, - /// then drives [`LaunchRuntime::launch`] on the bound executor (the - /// ambient tokio runtime by default). + /// then drives [`LaunchRuntime::launch`] on the bound executor + /// ([`TokioExecutor`] by default). pub async fn launch(self) -> anyhow::Result { let data_dir = self.config.engine.state_dir.clone(); let build_ctx = BuilderContext { @@ -368,11 +362,10 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> { }; let components = R::components().build::(&build_ctx).await?; - // The preset owns its add-ons; the launcher borrows each one to - // install it, so both the owned set and the ref view stay live across - // the launch await. + // `add_ons` owns the boxed add-ons; `add_on_refs` borrows into it and is + // consumed by the launch call, so both must stay in scope for that call. let add_ons = R::add_ons(); - let add_on_refs: Vec<&dyn RuntimeAddOns> = add_ons.iter().map(|a| &**a).collect(); + let add_on_refs: Vec<&dyn RuntimeAddOn> = add_ons.iter().map(|a| &**a).collect(); let runtime = AssembledRuntime { components, @@ -382,9 +375,11 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> { manifest: self.manifest.as_deref(), clocks: self.clocks, }; + // A named local keeps the default's borrow unambiguous (not a + // temporary); `with_executor` overrides it. + let default_executor = TokioExecutor; let ctx = LaunchContext { - executor: self.executor.unwrap_or(&TokioExecutor), - data_dir: &data_dir, + executor: self.executor.unwrap_or(&default_executor), config: self.config, }; runtime.launch(ctx).await @@ -418,8 +413,8 @@ impl<'a, T: RuntimeTypes> TypedBuilder<'a, T> { self } - /// Bind the executor the launcher spawns its tasks on. Defaults to the - /// ambient tokio runtime. + /// Bind the executor the launcher spawns its tasks on. Defaults to + /// [`TokioExecutor`], which spawns on the ambient tokio runtime. pub fn with_executor(mut self, executor: &'a dyn TaskExecutor) -> Self { self.executor = Some(executor); self @@ -465,10 +460,7 @@ pub struct ComponentsStage<'a, T: RuntimeTypes, C, S, E> { impl<'a, T: RuntimeTypes, C, S, E> ComponentsStage<'a, T, C, S, E> { /// Bind the cross-cutting add-on set installed before the engine boots. - pub fn with_add_ons( - self, - add_ons: &'a [&'a dyn RuntimeAddOns], - ) -> ReadyBuilder<'a, T, C, S, E> { + pub fn with_add_ons(self, add_ons: &'a [&'a dyn RuntimeAddOn]) -> ReadyBuilder<'a, T, C, S, E> { ReadyBuilder { config: self.config, extensions: self.extensions, @@ -492,7 +484,7 @@ pub struct ReadyBuilder<'a, T: RuntimeTypes, C, S, E> { executor: Option<&'a dyn TaskExecutor>, clocks: Option, components: ComponentsBuilder, - add_ons: &'a [&'a dyn RuntimeAddOns], + add_ons: &'a [&'a dyn RuntimeAddOn], } impl ReadyBuilder<'_, T, C, S, E> @@ -504,7 +496,7 @@ where { /// Open the backends and launch. Builds the [`Components`] bundle from the /// bound builders, then drives [`LaunchRuntime::launch`] on the bound - /// executor (the ambient tokio runtime by default). + /// executor ([`TokioExecutor`] by default). pub async fn launch(self) -> anyhow::Result { let data_dir = self.config.engine.state_dir.clone(); let build_ctx = BuilderContext { @@ -521,9 +513,11 @@ where manifest: self.manifest.as_deref(), clocks: self.clocks, }; + // A named local keeps the default's borrow unambiguous (not a + // temporary); `with_executor` overrides it. + let default_executor = TokioExecutor; let ctx = LaunchContext { - executor: self.executor.unwrap_or(&TokioExecutor), - data_dir: &data_dir, + executor: self.executor.unwrap_or(&default_executor), config: self.config, }; runtime.launch(ctx).await @@ -568,7 +562,7 @@ mod tests { #[tokio::test] async fn assembled_runtime_installs_add_ons_before_boot() { struct CountingAddOn(Arc); - impl RuntimeAddOns for CountingAddOn { + impl RuntimeAddOn for CountingAddOn { fn install(&self, _ctx: &AddOnsContext<'_>) -> anyhow::Result { self.0.fetch_add(1, Ordering::SeqCst); Ok(AddOnHandle::named("counting")) @@ -591,7 +585,7 @@ mod tests { let calls = Arc::new(AtomicUsize::new(0)); let add_on = CountingAddOn(calls.clone()); - let add_on_refs: Vec<&dyn RuntimeAddOns> = vec![&add_on]; + let add_on_refs: Vec<&dyn RuntimeAddOn> = vec![&add_on]; let runtime = AssembledRuntime { components, extensions: Vec::new(), @@ -603,7 +597,6 @@ mod tests { let executor = TokioExecutor; let ctx = LaunchContext { executor: &executor, - data_dir: &data_dir, config: &config, }; diff --git a/crates/nexum-runtime/src/host/component/builder.rs b/crates/nexum-runtime/src/host/component/builder.rs index 99179148..1657e5e9 100644 --- a/crates/nexum-runtime/src/host/component/builder.rs +++ b/crates/nexum-runtime/src/host/component/builder.rs @@ -73,6 +73,22 @@ impl ComponentBuilder for LocalStoreBuilder { } } +/// Names the component slot whose build failed. The leaf cause stays an +/// `anyhow::Error` because the backends fail for heterogeneous reasons +/// (I/O for the store, network for the chain). +#[derive(Debug, thiserror::Error)] +pub enum BuildError { + /// The chain backend builder failed. + #[error("build the chain backend: {0}")] + Chain(anyhow::Error), + /// The store backend builder failed. + #[error("build the store backend: {0}")] + Store(anyhow::Error), + /// The extension payload builder failed. + #[error("build the extension payload: {0}")] + Ext(anyhow::Error), +} + /// The empty extension payload: a no-op builder for a core-only lattice /// (`Ext = ()`). impl ComponentBuilder for () { @@ -105,17 +121,18 @@ impl ComponentsBuilder { /// Drive each builder against `ctx`, then bundle the backends with a /// fresh log pipeline. The builder outputs must match the lattice /// seams: chain to [`RuntimeTypes::Chain`], store to - /// [`RuntimeTypes::Store`], ext to [`RuntimeTypes::Ext`]. - pub async fn build(self, ctx: &BuilderContext<'_>) -> anyhow::Result> + /// [`RuntimeTypes::Store`], ext to [`RuntimeTypes::Ext`]. A failing + /// sub-build returns the [`BuildError`] variant naming that slot. + pub async fn build(self, ctx: &BuilderContext<'_>) -> Result, BuildError> where T: RuntimeTypes, C: ComponentBuilder, S: ComponentBuilder, E: ComponentBuilder, { - let chain = self.chain.build(ctx).await?; - let store = self.store.build(ctx).await?; - let ext = self.ext.build(ctx).await?; + let chain = self.chain.build(ctx).await.map_err(BuildError::Chain)?; + let store = self.store.build(ctx).await.map_err(BuildError::Store)?; + let ext = self.ext.build(ctx).await.map_err(BuildError::Ext)?; let logs = LogPipeline::in_memory(ctx.config.limits.logs()); Ok(Components { chain, diff --git a/crates/nexum-runtime/src/host/component/mod.rs b/crates/nexum-runtime/src/host/component/mod.rs index eb5bca2d..0adb15fa 100644 --- a/crates/nexum-runtime/src/host/component/mod.rs +++ b/crates/nexum-runtime/src/host/component/mod.rs @@ -10,7 +10,8 @@ mod runtime_types; mod state; pub use builder::{ - BuilderContext, ComponentBuilder, ComponentsBuilder, LocalStoreBuilder, ProviderPoolBuilder, + BuildError, BuilderContext, ComponentBuilder, ComponentsBuilder, LocalStoreBuilder, + ProviderPoolBuilder, }; pub use chain::{ChainMethod, ChainProvider}; pub use runtime_types::{Handle, RuntimeTypes};