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
58 changes: 58 additions & 0 deletions crates/nexum-runtime/src/host/actor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! The supervised host-actor primitive: one component instance the host
//! holds and others call. The store is refuelled before each guest call,
//! a trap is projected onto a typed fault instead of unwinding into the
//! caller, and each instance sits behind an [`ActorSlot`] async mutex held
//! across the guest await, so one store never runs two guest calls at once.

use std::sync::Arc;

use tokio::sync::Mutex as AsyncMutex;
use wasmtime::Store;

use super::component::RuntimeTypes;
use super::state::HostState;

/// One supervised actor behind its serialising mutex. A wasmtime `Store`
/// is not `Sync`; concurrent callers queue here.
pub type ActorSlot<A> = Arc<AsyncMutex<A>>;

/// A guest call failed outside the component's typed error space.
#[derive(Debug, thiserror::Error)]
pub enum ActorFault {
/// The pre-call refuel failed; the guest was never entered.
#[error("refuel failed: {0}")]
Refuel(wasmtime::Error),
/// The guest trapped. Carries the root cause only; the wasm frame
/// list stays out of the caller-facing message.
#[error("trapped: {}", .0.root_cause())]
Trap(wasmtime::Error),
}

/// A supervised component store: refuelled before each guest call so every
/// invocation starts from a full budget, with traps projected onto
/// [`ActorFault`].
pub struct SupervisedStore<T: RuntimeTypes> {
store: Store<HostState<T>>,
fuel_per_call: u64,
}

impl<T: RuntimeTypes> SupervisedStore<T> {
/// Supervise an instantiated store with a per-call fuel budget.
pub fn new(store: Store<HostState<T>>, fuel_per_call: u64) -> Self {
Self {
store,
fuel_per_call,
}
}

/// Refuel, then run one guest call against the store.
pub async fn call<R>(
&mut self,
call: impl AsyncFnOnce(&mut Store<HostState<T>>) -> wasmtime::Result<R>,
) -> Result<R, ActorFault> {
self.store
.set_fuel(self.fuel_per_call)
.map_err(ActorFault::Refuel)?;
call(&mut self.store).await.map_err(ActorFault::Trap)
}
}
45 changes: 38 additions & 7 deletions crates/nexum-runtime/src/host/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,46 @@ pub trait ProviderKind<T: RuntimeTypes>: Send + Sync + 'static {
/// Adds the provider's imports to a provider linker.
fn link(&self, linker: &mut Linker<HostState<T>>) -> anyhow::Result<()>;

/// Install one instantiated provider behind the extension's service.
/// Instantiate one provider and install it behind the owning service.
/// [`Installed::Dead`] reports a failed guest `init`; an `Err` is a
/// boot error.
async fn install(
&self,
component: &Component,
store: Store<HostState<T>>,
instance: ProviderInstance<'_, T>,
service: &Arc<dyn HostService>,
) -> anyhow::Result<()>;
) -> anyhow::Result<Installed>;
}

/// One provider instance ready to install: the compiled component, the
/// linker the kind's [`ProviderKind::link`] populated, the supervised
/// store, the manifest `[config]`, and the per-call fuel budget.
pub struct ProviderInstance<'a, T: RuntimeTypes> {
/// Compiled provider component.
pub component: &'a Component,
/// Linker carrying the kind's imports plus the WASI base.
pub linker: &'a Linker<HostState<T>>,
/// Store the instance runs in; the kind takes ownership.
pub store: Store<HostState<T>>,
/// Manifest `[config]` handed to the guest `init`.
pub config: Vec<(String, String)>,
/// Fuel budget applied before each routed guest call.
pub fuel_per_call: u64,
}

/// Outcome of one provider install.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Installed {
/// `init` succeeded; the instance is installed and routable.
Live,
/// `init` returned a fault; the instance is loaded but not routable.
Dead,
}

/// Downcast a type-erased service to `S`. `None` when the type differs.
pub fn downcast_service<S: HostService>(service: &Arc<dyn HostService>) -> Option<Arc<S>> {
let service = Arc::clone(service);
let erased: Arc<dyn Any + Send + Sync> = service;
erased.downcast().ok()
}

/// Immutable per-namespace service map: each extension's [`HostService`]
Expand Down Expand Up @@ -103,9 +136,7 @@ impl HostServices {
/// The service under `namespace`, downcast to its concrete type.
/// `None` when the namespace is absent or the type does not match.
pub fn get<S: HostService>(&self, namespace: &str) -> Option<Arc<S>> {
let service = Arc::clone(self.0.get(namespace)?);
let erased: Arc<dyn Any + Send + Sync> = service;
erased.downcast().ok()
downcast_service(self.0.get(namespace)?)
}

/// The raw type-erased service under `namespace`.
Expand Down
3 changes: 3 additions & 0 deletions crates/nexum-runtime/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
//! namespace) an extension is wired in through at the composition root.
//! Domain extensions such as cow-api live in their own crates and plug
//! in through this seam rather than being hard-linked into the core host.
//! - [`actor`]: the supervised host-actor primitive provider instances
//! run behind (refuel, trap projection, serialising slot).
//! - [`http`]: the wasi:http outgoing gate enforcing the per-module
//! `[capabilities.http].allow` list.
//! - [`logs`]: the typed module-log pipeline (capture points -> router ->
//! tracing event + retention store) and its embedder read surface.

pub mod actor;
pub mod component;
pub mod error;
pub mod extension;
Expand Down
Loading
Loading