From 3782a36ba677bf8affeaf4d87d93cd76791341d6 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Tue, 30 Jul 2024 09:41:46 -0400 Subject: [PATCH] Add spin-factors-executor Signed-off-by: Lann Martin --- Cargo.lock | 13 ++ crates/core/src/lib.rs | 3 +- crates/factors-executor/Cargo.toml | 23 +++ crates/factors-executor/src/lib.rs | 223 +++++++++++++++++++++++++++++ 4 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 crates/factors-executor/Cargo.toml create mode 100644 crates/factors-executor/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d9769241d9..cb5949fac9 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/src/lib.rs b/crates/core/src/lib.rs index 411d96c513..e586c1bcb4 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -16,13 +16,12 @@ 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, }; diff --git a/crates/factors-executor/Cargo.toml b/crates/factors-executor/Cargo.toml new file mode 100644 index 0000000000..14500c6245 --- /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 0000000000..c624a02f02 --- /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)") + } + } +}