Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/nexum-cli/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

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::{
ComponentsBuilder, LocalStoreBuilder, ProviderPoolBuilder, RuntimeTypes,
};
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
Expand Down Expand Up @@ -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
Expand All @@ -51,6 +52,10 @@ pub async fn run_from_config(
.with_types::<ReferenceTypes>()
.with_extensions([extension::<ReferenceTypes>()])
.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,
Expand Down
8 changes: 4 additions & 4 deletions crates/nexum-runtime/src/addons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -39,15 +39,15 @@ 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<AddOnHandle>;
}

/// 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<Box<dyn RuntimeAddOns>>;
pub type AddOns = Vec<Box<dyn RuntimeAddOn>>;

/// The Prometheus exporter add-on. With `[engine.metrics].enabled = true`
/// it binds an HTTP listener serving `/metrics`; otherwise it installs the
Expand All @@ -56,7 +56,7 @@ pub type AddOns = Vec<Box<dyn RuntimeAddOns>>;
/// 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<AddOnHandle> {
if ctx.metrics.enabled {
let addr: std::net::SocketAddr = ctx.metrics.bind_addr.parse().map_err(|e| {
Expand Down
5 changes: 2 additions & 3 deletions crates/nexum-runtime/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -34,7 +34,7 @@ pub async fn run<T: RuntimeTypes>(
manifest: Option<&Path>,
components: &Components<T>,
extensions: &[Extension<T>],
add_ons: &[&dyn RuntimeAddOns],
add_ons: &[&dyn RuntimeAddOn],
) -> anyhow::Result<()> {
let runtime = AssembledRuntime {
components: components.clone(),
Expand All @@ -47,7 +47,6 @@ pub async fn run<T: RuntimeTypes>(
let executor = TokioExecutor;
let ctx = LaunchContext {
executor: &executor,
data_dir: &engine_cfg.engine.state_dir,
config: engine_cfg,
};
runtime.launch(ctx).await?.wait().await
Expand Down
63 changes: 28 additions & 35 deletions crates/nexum-runtime/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
Expand Down Expand Up @@ -98,7 +92,7 @@ pub struct AssembledRuntime<'a, T: RuntimeTypes> {
/// Linker hooks and capability namespaces.
pub extensions: Vec<Extension<T>>,
/// 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`.
Expand Down Expand Up @@ -231,8 +225,8 @@ impl<T: RuntimeTypes> 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"),
Expand All @@ -247,7 +241,7 @@ impl<T: RuntimeTypes> 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,
Expand Down Expand Up @@ -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
Expand All @@ -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<RuntimeHandle> {
let data_dir = self.config.engine.state_dir.clone();
let build_ctx = BuilderContext {
Expand All @@ -368,11 +362,10 @@ impl<'a, R: Runtime> PresetBuilder<'a, R> {
};
let components = R::components().build::<R::Types>(&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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -492,7 +484,7 @@ pub struct ReadyBuilder<'a, T: RuntimeTypes, C, S, E> {
executor: Option<&'a dyn TaskExecutor>,
clocks: Option<WasiClockOverride>,
components: ComponentsBuilder<C, S, E>,
add_ons: &'a [&'a dyn RuntimeAddOns],
add_ons: &'a [&'a dyn RuntimeAddOn],
}

impl<T, C, S, E> ReadyBuilder<'_, T, C, S, E>
Expand All @@ -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<RuntimeHandle> {
let data_dir = self.config.engine.state_dir.clone();
let build_ctx = BuilderContext {
Expand All @@ -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
Expand Down Expand Up @@ -568,7 +562,7 @@ mod tests {
#[tokio::test]
async fn assembled_runtime_installs_add_ons_before_boot() {
struct CountingAddOn(Arc<AtomicUsize>);
impl RuntimeAddOns for CountingAddOn {
impl RuntimeAddOn for CountingAddOn {
fn install(&self, _ctx: &AddOnsContext<'_>) -> anyhow::Result<AddOnHandle> {
self.0.fetch_add(1, Ordering::SeqCst);
Ok(AddOnHandle::named("counting"))
Expand All @@ -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(),
Expand All @@ -603,7 +597,6 @@ mod tests {
let executor = TokioExecutor;
let ctx = LaunchContext {
executor: &executor,
data_dir: &data_dir,
config: &config,
};

Expand Down
27 changes: 22 additions & 5 deletions crates/nexum-runtime/src/host/component/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -105,17 +121,18 @@ impl<C, S, E> ComponentsBuilder<C, S, E> {
/// 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<T>(self, ctx: &BuilderContext<'_>) -> anyhow::Result<Components<T>>
/// [`RuntimeTypes::Store`], ext to [`RuntimeTypes::Ext`]. A failing
/// sub-build returns the [`BuildError`] variant naming that slot.
pub async fn build<T>(self, ctx: &BuilderContext<'_>) -> Result<Components<T>, BuildError>
where
T: RuntimeTypes,
C: ComponentBuilder<Output = T::Chain>,
S: ComponentBuilder<Output = T::Store>,
E: ComponentBuilder<Output = T::Ext>,
{
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,
Expand Down
3 changes: 2 additions & 1 deletion crates/nexum-runtime/src/host/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down