spin_factors_executor/
lib.rs

1use std::{collections::HashMap, sync::Arc};
2
3use anyhow::Context;
4use spin_app::{App, AppComponent};
5use spin_core::{async_trait, Component};
6use spin_factors::{
7    AsInstanceState, ConfiguredApp, Factor, HasInstanceBuilder, RuntimeFactors,
8    RuntimeFactorsInstanceState,
9};
10
11/// A FactorsExecutor manages execution of a Spin app.
12///
13/// It is generic over the executor's [`RuntimeFactors`]. Additionally, it
14/// holds any other per-instance state needed by the caller.
15pub struct FactorsExecutor<T: RuntimeFactors, U: 'static = ()> {
16    core_engine: spin_core::Engine<InstanceState<T::InstanceState, U>>,
17    factors: T,
18    hooks: Vec<Box<dyn ExecutorHooks<T, U>>>,
19}
20
21impl<T: RuntimeFactors, U: Send + 'static> FactorsExecutor<T, U> {
22    /// Constructs a new executor.
23    pub fn new(
24        mut core_engine_builder: spin_core::EngineBuilder<
25            InstanceState<<T as RuntimeFactors>::InstanceState, U>,
26        >,
27        mut factors: T,
28    ) -> anyhow::Result<Self> {
29        factors
30            .init(core_engine_builder.linker())
31            .context("failed to initialize factors")?;
32        Ok(Self {
33            factors,
34            core_engine: core_engine_builder.build(),
35            hooks: Default::default(),
36        })
37    }
38
39    pub fn core_engine(&self) -> &spin_core::Engine<InstanceState<T::InstanceState, U>> {
40        &self.core_engine
41    }
42
43    // Adds the given [`ExecutorHooks`] to this executor.
44    ///
45    /// Hooks are run in the order they are added.
46    pub fn add_hooks(&mut self, hooks: impl ExecutorHooks<T, U> + 'static) {
47        self.hooks.push(Box::new(hooks));
48    }
49
50    /// Loads a [`App`] with this executor.
51    pub async fn load_app(
52        self: Arc<Self>,
53        app: App,
54        runtime_config: T::RuntimeConfig,
55        component_loader: &impl ComponentLoader<T, U>,
56        trigger_type: Option<&str>,
57    ) -> anyhow::Result<FactorsExecutorApp<T, U>> {
58        let configured_app = self
59            .factors
60            .configure_app(app, runtime_config)
61            .context("failed to configure app")?;
62
63        for hooks in &self.hooks {
64            hooks.configure_app(&configured_app).await?;
65        }
66
67        let components = match trigger_type {
68            Some(trigger_type) => configured_app
69                .app()
70                .triggers_with_type(trigger_type)
71                .filter_map(|t| t.component().ok())
72                .collect::<Vec<_>>(),
73            None => configured_app.app().components().collect(),
74        };
75        let mut component_instance_pres = HashMap::with_capacity(components.len());
76
77        for component in components {
78            let instance_pre = component_loader
79                .load_instance_pre(&self.core_engine, &component)
80                .await?;
81            component_instance_pres.insert(component.id().to_string(), instance_pre);
82        }
83
84        Ok(FactorsExecutorApp {
85            executor: self.clone(),
86            configured_app,
87            component_instance_pres,
88        })
89    }
90}
91
92#[async_trait]
93pub trait ExecutorHooks<T, U>: Send + Sync
94where
95    T: RuntimeFactors,
96{
97    /// Configure app hooks run immediately after [`RuntimeFactors::configure_app`].
98    async fn configure_app(&self, configured_app: &ConfiguredApp<T>) -> anyhow::Result<()> {
99        let _ = configured_app;
100        Ok(())
101    }
102
103    /// Prepare instance hooks run immediately before [`FactorsExecutorApp::prepare`] returns.
104    fn prepare_instance(&self, builder: &mut FactorsInstanceBuilder<T, U>) -> anyhow::Result<()> {
105        let _ = builder;
106        Ok(())
107    }
108}
109
110/// A ComponentLoader is responsible for loading Wasmtime [`Component`]s.
111#[async_trait]
112pub trait ComponentLoader<T: RuntimeFactors, U>: Sync {
113    /// Loads a [`Component`] for the given [`AppComponent`].
114    async fn load_component(
115        &self,
116        engine: &spin_core::wasmtime::Engine,
117        component: &AppComponent,
118    ) -> anyhow::Result<Component>;
119
120    /// Loads [`InstancePre`] for the given [`AppComponent`].
121    async fn load_instance_pre(
122        &self,
123        engine: &spin_core::Engine<InstanceState<T::InstanceState, U>>,
124        component: &AppComponent,
125    ) -> anyhow::Result<spin_core::InstancePre<InstanceState<T::InstanceState, U>>> {
126        let component = self.load_component(engine.as_ref(), component).await?;
127        engine.instantiate_pre(&component)
128    }
129}
130
131type InstancePre<T, U> =
132    spin_core::InstancePre<InstanceState<<T as RuntimeFactors>::InstanceState, U>>;
133
134/// A FactorsExecutorApp represents a loaded Spin app, ready for instantiation.
135///
136/// It is generic over the executor's [`RuntimeFactors`] and any ad-hoc additional
137/// per-instance state needed by the caller.
138pub struct FactorsExecutorApp<T: RuntimeFactors, U: 'static> {
139    executor: Arc<FactorsExecutor<T, U>>,
140    configured_app: ConfiguredApp<T>,
141    // Maps component IDs -> InstancePres
142    component_instance_pres: HashMap<String, InstancePre<T, U>>,
143}
144
145impl<T: RuntimeFactors, U: Send + 'static> FactorsExecutorApp<T, U> {
146    pub fn engine(&self) -> &spin_core::Engine<InstanceState<T::InstanceState, U>> {
147        &self.executor.core_engine
148    }
149
150    pub fn configured_app(&self) -> &ConfiguredApp<T> {
151        &self.configured_app
152    }
153
154    pub fn app(&self) -> &App {
155        self.configured_app.app()
156    }
157
158    pub fn get_component(&self, component_id: &str) -> anyhow::Result<&Component> {
159        Ok(self.get_instance_pre(component_id)?.component())
160    }
161
162    pub fn get_instance_pre(&self, component_id: &str) -> anyhow::Result<&InstancePre<T, U>> {
163        self.component_instance_pres
164            .get(component_id)
165            .with_context(|| format!("no such component {component_id:?}"))
166    }
167
168    /// Returns an instance builder for the given component ID.
169    pub fn prepare(&self, component_id: &str) -> anyhow::Result<FactorsInstanceBuilder<'_, T, U>> {
170        let app_component = self
171            .configured_app
172            .app()
173            .get_component(component_id)
174            .with_context(|| format!("no such component {component_id:?}"))?;
175
176        let instance_pre = self.component_instance_pres.get(component_id).unwrap();
177
178        let factor_builders = self
179            .executor
180            .factors
181            .prepare(&self.configured_app, component_id)?;
182
183        let store_builder = self.executor.core_engine.store_builder();
184
185        let mut builder = FactorsInstanceBuilder {
186            store_builder,
187            factor_builders,
188            instance_pre,
189            app_component,
190            factors: &self.executor.factors,
191        };
192
193        for hooks in &self.executor.hooks {
194            hooks.prepare_instance(&mut builder)?;
195        }
196
197        Ok(builder)
198    }
199}
200
201/// A FactorsInstanceBuilder manages the instantiation of a Spin component instance.
202///
203/// It is generic over the executor's [`RuntimeFactors`] and any ad-hoc additional
204/// per-instance state needed by the caller.
205pub struct FactorsInstanceBuilder<'a, F: RuntimeFactors, U: 'static> {
206    app_component: AppComponent<'a>,
207    store_builder: spin_core::StoreBuilder,
208    factor_builders: F::InstanceBuilders,
209    instance_pre: &'a InstancePre<F, U>,
210    factors: &'a F,
211}
212
213impl<T: RuntimeFactors, U: 'static> FactorsInstanceBuilder<'_, T, U> {
214    /// Returns the app component for the instance.
215    pub fn app_component(&self) -> &AppComponent<'_> {
216        &self.app_component
217    }
218
219    /// Returns the store builder for the instance.
220    pub fn store_builder(&mut self) -> &mut spin_core::StoreBuilder {
221        &mut self.store_builder
222    }
223
224    /// Returns the factor instance builders for the instance.
225    pub fn factor_builders(&mut self) -> &mut T::InstanceBuilders {
226        &mut self.factor_builders
227    }
228
229    /// Returns the specific instance builder for the given factor.
230    pub fn factor_builder<F: Factor>(&mut self) -> Option<&mut F::InstanceBuilder> {
231        self.factor_builders().for_factor::<F>()
232    }
233
234    /// Returns the underlying wasmtime engine for the instance.
235    pub fn wasmtime_engine(&self) -> &spin_core::WasmtimeEngine {
236        self.instance_pre.engine()
237    }
238
239    /// Returns the compiled component for the instance.
240    pub fn component(&self) -> &Component {
241        self.instance_pre.component()
242    }
243}
244
245impl<T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'_, T, U> {
246    /// Instantiates the instance with the given executor instance state
247    pub async fn instantiate(
248        self,
249        executor_instance_state: U,
250    ) -> anyhow::Result<(
251        spin_core::Instance,
252        spin_core::Store<InstanceState<T::InstanceState, U>>,
253    )> {
254        let instance_state = InstanceState {
255            core: Default::default(),
256            factors: self.factors.build_instance_state(self.factor_builders)?,
257            executor: executor_instance_state,
258        };
259        let mut store = self.store_builder.build(instance_state)?;
260        let instance = self.instance_pre.instantiate_async(&mut store).await?;
261        Ok((instance, store))
262    }
263}
264
265/// InstanceState is the [`spin_core::Store`] `data` for an instance.
266///
267/// It is generic over the [`RuntimeFactors::InstanceState`] and any ad-hoc
268/// data needed by the caller.
269pub struct InstanceState<T, U> {
270    core: spin_core::State,
271    factors: T,
272    executor: U,
273}
274
275impl<T, U> InstanceState<T, U> {
276    /// Provides access to the [`spin_core::State`].
277    pub fn core_state(&self) -> &spin_core::State {
278        &self.core
279    }
280
281    /// Provides mutable access to the [`spin_core::State`].
282    pub fn core_state_mut(&mut self) -> &mut spin_core::State {
283        &mut self.core
284    }
285
286    /// Provides access to the [`RuntimeFactors::InstanceState`].
287    pub fn factors_instance_state(&self) -> &T {
288        &self.factors
289    }
290
291    /// Provides mutable access to the [`RuntimeFactors::InstanceState`].
292    pub fn factors_instance_state_mut(&mut self) -> &mut T {
293        &mut self.factors
294    }
295
296    /// Provides access to the ad-hoc executor instance state.
297    pub fn executor_instance_state(&self) -> &U {
298        &self.executor
299    }
300
301    /// Provides mutable access to the ad-hoc executor instance state.
302    pub fn executor_instance_state_mut(&mut self) -> &mut U {
303        &mut self.executor
304    }
305}
306
307impl<T, U> spin_core::AsState for InstanceState<T, U> {
308    fn as_state(&mut self) -> &mut spin_core::State {
309        &mut self.core
310    }
311}
312
313impl<T: RuntimeFactorsInstanceState, U> AsInstanceState<T> for InstanceState<T, U> {
314    fn as_instance_state(&mut self) -> &mut T {
315        &mut self.factors
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use spin_factor_wasi::{DummyFilesMounter, WasiFactor};
322    use spin_factors::RuntimeFactors;
323    use spin_factors_test::TestEnvironment;
324
325    use super::*;
326
327    #[derive(RuntimeFactors)]
328    struct TestFactors {
329        wasi: WasiFactor,
330    }
331
332    #[tokio::test]
333    async fn instance_builder_works() -> anyhow::Result<()> {
334        let factors = TestFactors {
335            wasi: WasiFactor::new(DummyFilesMounter),
336        };
337        let env = TestEnvironment::new(factors);
338        let locked = env.build_locked_app().await?;
339        let app = App::new("test-app", locked);
340
341        let engine_builder = spin_core::Engine::builder(&Default::default())?;
342        let executor = Arc::new(FactorsExecutor::new(engine_builder, env.factors)?);
343
344        let factors_app = executor
345            .load_app(app, Default::default(), &DummyComponentLoader, None)
346            .await?;
347
348        let mut instance_builder = factors_app.prepare("empty")?;
349
350        assert_eq!(instance_builder.app_component().id(), "empty");
351
352        instance_builder.store_builder().max_memory_size(1_000_000);
353
354        instance_builder
355            .factor_builder::<WasiFactor>()
356            .unwrap()
357            .args(["foo"]);
358
359        let (_instance, _store) = instance_builder.instantiate(()).await?;
360        Ok(())
361    }
362
363    struct DummyComponentLoader;
364
365    #[async_trait]
366    impl ComponentLoader<TestFactors, ()> for DummyComponentLoader {
367        async fn load_component(
368            &self,
369            engine: &spin_core::wasmtime::Engine,
370            _component: &AppComponent,
371        ) -> anyhow::Result<Component> {
372            Component::new(engine, "(component)")
373        }
374    }
375}