diff --git a/Cargo.lock b/Cargo.lock index d9769241d..cb5949fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7776,6 +7776,19 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "spin-factors-executor" +version = "2.7.0-pre0" +dependencies = [ + "anyhow", + "spin-app", + "spin-core", + "spin-factor-wasi", + "spin-factors", + "spin-factors-test", + "tokio", +] + [[package]] name = "spin-factors-test" version = "2.7.0-pre0" diff --git a/crates/core/build.rs b/crates/core/build.rs new file mode 100644 index 000000000..c96556b06 --- /dev/null +++ b/crates/core/build.rs @@ -0,0 +1,6 @@ +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + // Enable spin-factors-derive to emit expanded macro output. + let out_dir = std::env::var("OUT_DIR").unwrap(); + println!("cargo:rustc-env=SPIN_FACTORS_DERIVE_EXPAND_DIR={out_dir}"); +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index cd02404ba..e586c1bcb 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -16,17 +16,16 @@ use std::{path::PathBuf, time::Duration}; use anyhow::Result; use crossbeam_channel::Sender; use tracing::instrument; -use wasmtime::component::{InstancePre, Linker}; use wasmtime::{InstanceAllocationStrategy, PoolingAllocationConfig}; pub use async_trait::async_trait; pub use wasmtime::{ self, - component::{Component, Instance}, + component::{Component, Instance, InstancePre, Linker}, Instance as ModuleInstance, Module, Trap, }; -pub use store::{Store, StoreBuilder}; +pub use store::{AsState, Store, StoreBuilder}; /// The default [`EngineBuilder::epoch_tick_interval`]. pub const DEFAULT_EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(10); diff --git a/crates/core/src/store.rs b/crates/core/src/store.rs index b2b950b5f..7ad7168b5 100644 --- a/crates/core/src/store.rs +++ b/crates/core/src/store.rs @@ -97,11 +97,11 @@ impl StoreBuilder { /// /// The `T` parameter must provide access to a [`State`] via `impl /// AsMut`. - pub fn build>(self, mut data: T) -> Result> { - data.as_mut().store_limits = self.store_limits; + pub fn build(self, mut data: T) -> Result> { + data.as_state().store_limits = self.store_limits; let mut inner = wasmtime::Store::new(&self.engine, data); - inner.limiter_async(|data| &mut data.as_mut().store_limits); + inner.limiter_async(|data| &mut data.as_state().store_limits); // With epoch interruption enabled, there must be _some_ deadline set // or execution will trap immediately. Since this is a delta, we need @@ -115,3 +115,16 @@ impl StoreBuilder { }) } } + +/// For consumers that need to use a type other than [`State`] as the [`Store`] +/// `data`, this trait must be implemented for that type. +pub trait AsState { + /// Gives access to the inner [`State`]. + fn as_state(&mut self) -> &mut State; +} + +impl AsState for State { + fn as_state(&mut self) -> &mut State { + self + } +} diff --git a/crates/core/tests/integration_test.rs b/crates/core/tests/integration_test.rs index da328b2c5..8db91940d 100644 --- a/crates/core/tests/integration_test.rs +++ b/crates/core/tests/integration_test.rs @@ -5,9 +5,9 @@ use std::{ use anyhow::Context; use serde_json::json; -use spin_core::{Component, Config, Engine, State, Store, StoreBuilder, Trap}; +use spin_core::{AsState, Component, Config, Engine, State, Store, StoreBuilder, Trap}; use spin_factor_wasi::{DummyFilesMounter, WasiFactor}; -use spin_factors::{App, RuntimeFactors}; +use spin_factors::{App, AsInstanceState, RuntimeFactors}; use spin_locked_app::locked::LockedApp; use tokio::{fs, io::AsyncWrite}; use wasmtime_wasi::I32Exit; @@ -93,14 +93,14 @@ struct TestState { factors: TestFactorsInstanceState, } -impl AsMut for TestState { - fn as_mut(&mut self) -> &mut State { +impl AsState for TestState { + fn as_state(&mut self) -> &mut State { &mut self.core } } -impl AsMut for TestState { - fn as_mut(&mut self) -> &mut TestFactorsInstanceState { +impl AsInstanceState for TestState { + fn as_instance_state(&mut self) -> &mut TestFactorsInstanceState { &mut self.factors } } diff --git a/crates/factors-derive/src/lib.rs b/crates/factors-derive/src/lib.rs index 80b750d46..921196c8b 100644 --- a/crates/factors-derive/src/lib.rs +++ b/crates/factors-derive/src/lib.rs @@ -78,7 +78,7 @@ fn expand_factors(input: &DeriveInput) -> syn::Result { type InstanceState = #state_name; type RuntimeConfig = #runtime_config_name; - fn init + Send + 'static>( + fn init + Send + 'static>( &mut self, linker: &mut #wasmtime::component::Linker, ) -> #Result<()> { @@ -98,9 +98,9 @@ fn expand_factors(input: &DeriveInput) -> syn::Result { &mut self.#factor_names, #factors_path::InitContext::::new( linker, - |data| &mut data.as_mut().#factor_names, + |data| &mut data.as_instance_state().#factor_names, |data| { - let state = data.as_mut(); + let state = data.as_instance_state(); (&mut state.#factor_names, &mut state.__table) }, ) @@ -239,8 +239,8 @@ fn expand_factors(input: &DeriveInput) -> syn::Result { } } - impl AsMut<#state_name> for #state_name { - fn as_mut(&mut self) -> &mut Self { + impl #factors_path::AsInstanceState<#state_name> for #state_name { + fn as_instance_state(&mut self) -> &mut Self { self } } diff --git a/crates/factors-executor/Cargo.toml b/crates/factors-executor/Cargo.toml new file mode 100644 index 000000000..14500c624 --- /dev/null +++ b/crates/factors-executor/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "spin-factors-executor" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1" +spin-app = { path = "../app" } +spin-core = { path = "../core" } +spin-factors = { path = "../factors" } + +[dev-dependencies] +spin-factor-wasi = { path = "../factor-wasi" } +spin-factors-test = { path = "../factors-test" } +tokio = { version = "1", features = ["macros", "rt"] } + +[lints] +workspace = true diff --git a/crates/factors-executor/src/lib.rs b/crates/factors-executor/src/lib.rs new file mode 100644 index 000000000..c624a02f0 --- /dev/null +++ b/crates/factors-executor/src/lib.rs @@ -0,0 +1,223 @@ +use std::collections::HashMap; + +use anyhow::Context; +use spin_app::{App, AppComponent}; +use spin_core::Component; +use spin_factors::{AsInstanceState, ConfiguredApp, RuntimeFactors, RuntimeFactorsInstanceState}; + +/// A FactorsExecutor manages execution of a Spin app. +/// +/// `Factors` is the executor's [`RuntimeFactors`]. `ExecutorInstanceState` +/// holds any other per-instance state needed by the caller. +pub struct FactorsExecutor { + factors: T, + core_engine: spin_core::Engine>, + configured_app: ConfiguredApp, + // Maps component IDs -> InstancePres + component_instance_pres: HashMap>, +} + +type InstancePre = + spin_core::InstancePre::InstanceState, U>>; + +impl FactorsExecutor { + /// Constructs a new executor. + pub fn new( + core_config: &spin_core::Config, + mut factors: T, + app: App, + mut component_loader: impl ComponentLoader, + runtime_config: T::RuntimeConfig, + ) -> anyhow::Result { + let core_engine = { + let mut builder = + spin_core::Engine::builder(core_config).context("failed to initialize engine")?; + factors + .init(builder.linker()) + .context("failed to initialize factors")?; + builder.build() + }; + + let configured_app = factors + .configure_app(app, runtime_config) + .context("failed to configure app")?; + + let component_instance_pres = configured_app + .app() + .components() + .map(|app_component| { + let component = + component_loader.load_component(core_engine.as_ref(), &app_component)?; + let instance_pre = core_engine.instantiate_pre(&component)?; + Ok((app_component.id().to_string(), instance_pre)) + }) + .collect::>>()?; + + Ok(Self { + factors, + core_engine, + configured_app, + component_instance_pres, + }) + } + + /// Returns an instance builder for the given component ID. + pub fn prepare(&mut self, component_id: &str) -> anyhow::Result> { + let app_component = self + .configured_app + .app() + .get_component(component_id) + .with_context(|| format!("no such component {component_id:?}"))?; + let instance_pre = self.component_instance_pres.get(component_id).unwrap(); + let factor_builders = self.factors.prepare(&self.configured_app, component_id)?; + let store_builder = self.core_engine.store_builder(); + Ok(FactorsInstanceBuilder { + store_builder, + factor_builders, + instance_pre, + app_component, + factors: &self.factors, + }) + } +} + +/// A ComponentLoader is responsible for loading Wasmtime [`Component`]s. +pub trait ComponentLoader { + /// Loads a [`Component`] for the given [`AppComponent`]. + fn load_component( + &mut self, + engine: &spin_core::wasmtime::Engine, + component: &AppComponent, + ) -> anyhow::Result; +} + +/// A FactorsInstanceBuilder manages the instantiation of a Spin component +/// instance. +pub struct FactorsInstanceBuilder<'a, T: RuntimeFactors, U> { + app_component: AppComponent<'a>, + store_builder: spin_core::StoreBuilder, + factor_builders: T::InstanceBuilders, + instance_pre: &'a InstancePre, + factors: &'a T, +} + +impl<'a, T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'a, T, U> { + /// Returns the app component for the instance. + pub fn app_component(&self) -> &AppComponent { + &self.app_component + } + + /// Returns the store builder for the instance. + pub fn store_builder(&mut self) -> &mut spin_core::StoreBuilder { + &mut self.store_builder + } + + /// Returns the factor instance builders for the instance. + pub fn factor_builders(&mut self) -> &mut T::InstanceBuilders { + &mut self.factor_builders + } + + /// Instantiates the instance with the given executor instance state + pub async fn instantiate( + self, + executor_instance_state: U, + ) -> anyhow::Result<( + spin_core::Instance, + spin_core::Store>, + )> { + let instance_state = InstanceState { + core: Default::default(), + factors: self.factors.build_instance_state(self.factor_builders)?, + executor: executor_instance_state, + }; + let mut store = self.store_builder.build(instance_state)?; + let instance = self.instance_pre.instantiate_async(&mut store).await?; + Ok((instance, store)) + } +} + +/// InstanceState is the [`spin_core::Store`] `data` for an instance. +pub struct InstanceState { + core: spin_core::State, + factors: FactorsState, + executor: ExecutorInstanceState, +} + +impl InstanceState { + /// Provides access to the `ExecutorInstanceState`. + pub fn executor_instance_state(&mut self) -> &mut U { + &mut self.executor + } +} + +impl spin_core::AsState for InstanceState { + fn as_state(&mut self) -> &mut spin_core::State { + &mut self.core + } +} + +impl AsInstanceState for InstanceState { + fn as_instance_state(&mut self) -> &mut T { + &mut self.factors + } +} + +#[cfg(test)] +mod tests { + use spin_factor_wasi::{DummyFilesMounter, WasiFactor}; + use spin_factors::RuntimeFactors; + use spin_factors_test::TestEnvironment; + + use super::*; + + #[derive(RuntimeFactors)] + struct TestFactors { + wasi: WasiFactor, + } + + #[tokio::test] + async fn instance_builder_works() -> anyhow::Result<()> { + let factors = TestFactors { + wasi: WasiFactor::new(DummyFilesMounter), + }; + let env = TestEnvironment::new(factors); + let locked = env.build_locked_app().await?; + let app = App::new("test-app", locked); + + let mut executor = FactorsExecutor::new( + &Default::default(), + env.factors, + app, + DummyComponentLoader, + Default::default(), + )?; + + let mut instance_builder = executor.prepare("empty")?; + + assert_eq!(instance_builder.app_component().id(), "empty"); + + instance_builder.store_builder().max_memory_size(1_000_000); + + instance_builder + .factor_builders() + .wasi + .as_mut() + .unwrap() + .args(["foo"]); + + let (_instance, _store) = instance_builder.instantiate(()).await?; + Ok(()) + } + + struct DummyComponentLoader; + + impl ComponentLoader for DummyComponentLoader { + fn load_component( + &mut self, + engine: &spin_core::wasmtime::Engine, + _component: &AppComponent, + ) -> anyhow::Result { + Component::new(engine, "(component)") + } + } +} diff --git a/crates/factors/src/lib.rs b/crates/factors/src/lib.rs index ce0666825..d73435f21 100644 --- a/crates/factors/src/lib.rs +++ b/crates/factors/src/lib.rs @@ -14,7 +14,7 @@ pub use crate::{ factor::{ConfigureAppContext, ConfiguredApp, Factor, FactorInstanceState, InitContext}, prepare::{FactorInstanceBuilder, InstanceBuilders, PrepareContext, SelfInstanceBuilder}, runtime_config::{FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer}, - runtime_factors::{RuntimeFactors, RuntimeFactorsInstanceState}, + runtime_factors::{AsInstanceState, RuntimeFactors, RuntimeFactorsInstanceState}, }; /// Result wrapper type defaulting to use [`Error`]. diff --git a/crates/factors/src/runtime_factors.rs b/crates/factors/src/runtime_factors.rs index a0ff6dd68..e18a66c17 100644 --- a/crates/factors/src/runtime_factors.rs +++ b/crates/factors/src/runtime_factors.rs @@ -44,7 +44,7 @@ pub trait RuntimeFactors: Sized + 'static { /// /// Each factor's `init` is called in turn. Must be called once before /// [`RuntimeFactors::prepare`]. - fn init + Send + 'static>( + fn init + Send + 'static>( &mut self, linker: &mut Linker, ) -> crate::Result<()>; @@ -84,7 +84,7 @@ pub trait RuntimeFactors: Sized + 'static { /// Get the state of a particular Factor from the overall InstanceState /// /// Implemented by `#[derive(RuntimeFactors)]` -pub trait RuntimeFactorsInstanceState: AsMut + Send + 'static { +pub trait RuntimeFactorsInstanceState: AsInstanceState + Send + 'static { fn get_with_table( &mut self, ) -> Option<(&mut FactorInstanceState, &mut ResourceTable)>; @@ -97,3 +97,7 @@ pub trait RuntimeFactorsInstanceState: AsMut + Send + 'static { fn table_mut(&mut self) -> &mut ResourceTable; } + +pub trait AsInstanceState { + fn as_instance_state(&mut self) -> &mut T; +} diff --git a/crates/factors/tests/smoke.rs b/crates/factors/tests/smoke.rs index f23a3d15d..7cbbb24b3 100644 --- a/crates/factors/tests/smoke.rs +++ b/crates/factors/tests/smoke.rs @@ -14,7 +14,8 @@ use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factor_variables::VariablesFactor; use spin_factor_wasi::{DummyFilesMounter, WasiFactor}; use spin_factors::{ - Factor, FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, RuntimeFactors, + AsInstanceState, Factor, FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, + RuntimeFactors, }; use wasmtime_wasi_http::WasiHttpView; @@ -32,8 +33,8 @@ struct Data { _other_data: usize, } -impl AsMut for Data { - fn as_mut(&mut self) -> &mut FactorsInstanceState { +impl AsInstanceState for Data { + fn as_instance_state(&mut self) -> &mut FactorsInstanceState { &mut self.factors_instance_state } } @@ -116,7 +117,8 @@ async fn smoke_test_works() -> anyhow::Result<()> { // Invoke handler let req = http::Request::get("/").body(Default::default()).unwrap(); - let mut wasi_http = OutboundHttpFactor::get_wasi_http_impl(store.data_mut().as_mut()).unwrap(); + let mut wasi_http = + OutboundHttpFactor::get_wasi_http_impl(store.data_mut().as_instance_state()).unwrap(); let request = wasi_http.new_incoming_request(req)?; let (response_tx, response_rx) = tokio::sync::oneshot::channel(); let response = wasi_http.new_response_outparam(response_tx)?;