Skip to main content

spin_factors_executor/
lib.rs

1use std::time::{Duration, Instant};
2use std::{collections::HashMap, sync::Arc};
3
4use anyhow::Context;
5use spin_app::{App, AppComponent};
6use spin_core::{Component, async_trait, wasmtime::CallHook};
7use spin_factors::{
8    AsInstanceState, ConfiguredApp, Factor, HasInstanceBuilder, RuntimeFactors,
9    RuntimeFactorsInstanceState,
10};
11
12/// A FactorsExecutor manages execution of a Spin app.
13///
14/// It is generic over the executor's [`RuntimeFactors`]. Additionally, it
15/// holds any other per-instance state needed by the caller.
16pub struct FactorsExecutor<T: RuntimeFactors, U: 'static = ()> {
17    core_engine: spin_core::Engine<InstanceState<T::InstanceState, U>>,
18    factors: T,
19    hooks: Vec<Box<dyn ExecutorHooks<T, U>>>,
20}
21
22impl<T: RuntimeFactors, U: Send + 'static> FactorsExecutor<T, U> {
23    /// Constructs a new executor.
24    pub fn new(
25        mut core_engine_builder: spin_core::EngineBuilder<
26            InstanceState<<T as RuntimeFactors>::InstanceState, U>,
27        >,
28        mut factors: T,
29    ) -> anyhow::Result<Self> {
30        factors
31            .init(core_engine_builder.linker())
32            .context("failed to initialize factors")?;
33        Ok(Self {
34            factors,
35            core_engine: core_engine_builder.build(),
36            hooks: Default::default(),
37        })
38    }
39
40    pub fn core_engine(&self) -> &spin_core::Engine<InstanceState<T::InstanceState, U>> {
41        &self.core_engine
42    }
43
44    // Adds the given [`ExecutorHooks`] to this executor.
45    ///
46    /// Hooks are run in the order they are added.
47    pub fn add_hooks(&mut self, hooks: impl ExecutorHooks<T, U> + 'static) {
48        self.hooks.push(Box::new(hooks));
49    }
50
51    /// Loads a [`App`] with this executor.
52    pub async fn load_app(
53        self: Arc<Self>,
54        app: App,
55        runtime_config: T::RuntimeConfig,
56        component_loader: &impl ComponentLoader<T, U>,
57        trigger_type: Option<&str>,
58        trigger_dependencies_composer: impl TriggerDependenciesComposer,
59    ) -> anyhow::Result<FactorsExecutorApp<T, U>> {
60        let configured_app = self
61            .factors
62            .configure_app(app, runtime_config)
63            .context("failed to configure app")?;
64
65        for hooks in &self.hooks {
66            hooks.configure_app(&configured_app).await?;
67        }
68
69        let components = match trigger_type {
70            Some(trigger_type) => configured_app
71                .app()
72                .triggers_with_type(trigger_type)
73                .filter_map(|t| t.component().ok())
74                .collect::<Vec<_>>(),
75            None => configured_app.app().components().collect(),
76        };
77        let mut component_instance_pres = HashMap::with_capacity(components.len());
78
79        for component in components {
80            let instance_pre = component_loader
81                .load_instance_pre(
82                    &self.core_engine,
83                    &component,
84                    &trigger_dependencies_composer,
85                )
86                .await?;
87            component_instance_pres.insert(component.id().to_string(), instance_pre);
88        }
89
90        Ok(FactorsExecutorApp {
91            executor: self.clone(),
92            configured_app,
93            component_instance_pres,
94        })
95    }
96}
97
98#[async_trait]
99pub trait ExecutorHooks<T, U>: Send + Sync
100where
101    T: RuntimeFactors,
102{
103    /// Configure app hooks run immediately after [`RuntimeFactors::configure_app`].
104    async fn configure_app(&self, configured_app: &ConfiguredApp<T>) -> anyhow::Result<()> {
105        let _ = configured_app;
106        Ok(())
107    }
108
109    /// Prepare instance hooks run immediately before [`FactorsExecutorApp::prepare`] returns.
110    fn prepare_instance(&self, builder: &mut FactorsInstanceBuilder<T, U>) -> anyhow::Result<()> {
111        let _ = builder;
112        Ok(())
113    }
114}
115
116/// A ComponentLoader is responsible for loading Wasmtime [`Component`]s.
117#[async_trait]
118pub trait ComponentLoader<T: RuntimeFactors, U>: Sync {
119    /// Loads a [`Component`] for the given [`AppComponent`].
120    async fn load_component(
121        &self,
122        engine: &spin_core::wasmtime::Engine,
123        component: &AppComponent,
124        trigger_dependencies_composer: &impl TriggerDependenciesComposer,
125    ) -> anyhow::Result<Component>;
126
127    /// Loads [`InstancePre`] for the given [`AppComponent`].
128    async fn load_instance_pre(
129        &self,
130        engine: &spin_core::Engine<InstanceState<T::InstanceState, U>>,
131        component: &AppComponent,
132        trigger_dependencies_composer: &impl TriggerDependenciesComposer,
133    ) -> anyhow::Result<spin_core::InstancePre<InstanceState<T::InstanceState, U>>> {
134        let component = self
135            .load_component(engine.as_ref(), component, trigger_dependencies_composer)
136            .await?;
137        engine.instantiate_pre(&component)
138    }
139}
140
141#[async_trait]
142pub trait TriggerDependenciesComposer: Send + Sync {
143    async fn compose_trigger_dependencies(
144        &self,
145        trigger_dependencies: &HashMap<String, Vec<TriggerDependency>>,
146        component: Vec<u8>,
147    ) -> anyhow::Result<Vec<u8>>;
148}
149
150#[async_trait]
151impl TriggerDependenciesComposer for () {
152    async fn compose_trigger_dependencies(
153        &self,
154        trigger_dependencies: &HashMap<String, Vec<TriggerDependency>>,
155        component: Vec<u8>,
156    ) -> anyhow::Result<Vec<u8>> {
157        if trigger_dependencies.is_empty() {
158            Ok(component)
159        } else {
160            Err(anyhow::anyhow!("this trigger should not have dependencies"))
161        }
162    }
163}
164
165pub struct TriggerDependency {
166    pub data: TriggerDependencyData,
167    pub dependency: spin_app::locked::LockedComponentDependency,
168}
169
170pub enum TriggerDependencyData {
171    InMemory(Vec<u8>),
172    OnDisk(std::path::PathBuf),
173}
174
175type InstancePre<T, U> =
176    spin_core::InstancePre<InstanceState<<T as RuntimeFactors>::InstanceState, U>>;
177
178/// A FactorsExecutorApp represents a loaded Spin app, ready for instantiation.
179///
180/// It is generic over the executor's [`RuntimeFactors`] and any ad-hoc additional
181/// per-instance state needed by the caller.
182pub struct FactorsExecutorApp<T: RuntimeFactors, U: 'static> {
183    executor: Arc<FactorsExecutor<T, U>>,
184    configured_app: ConfiguredApp<T>,
185    // Maps component IDs -> InstancePres
186    component_instance_pres: HashMap<String, InstancePre<T, U>>,
187}
188
189impl<T: RuntimeFactors, U: Send + 'static> FactorsExecutorApp<T, U> {
190    pub fn engine(&self) -> &spin_core::Engine<InstanceState<T::InstanceState, U>> {
191        &self.executor.core_engine
192    }
193
194    pub fn configured_app(&self) -> &ConfiguredApp<T> {
195        &self.configured_app
196    }
197
198    pub fn app(&self) -> &App {
199        self.configured_app.app()
200    }
201
202    pub fn get_component(&self, component_id: &str) -> anyhow::Result<&Component> {
203        Ok(self.get_instance_pre(component_id)?.component())
204    }
205
206    pub fn get_instance_pre(&self, component_id: &str) -> anyhow::Result<&InstancePre<T, U>> {
207        self.component_instance_pres
208            .get(component_id)
209            .with_context(|| format!("no such component {component_id:?}"))
210    }
211
212    /// Returns an instance builder for the given component ID.
213    pub fn prepare(&self, component_id: &str) -> anyhow::Result<FactorsInstanceBuilder<'_, T, U>> {
214        let app_component = self
215            .configured_app
216            .app()
217            .get_component(component_id)
218            .with_context(|| format!("no such component {component_id:?}"))?;
219
220        let instance_pre = self.component_instance_pres.get(component_id).unwrap();
221
222        let factor_builders = self
223            .executor
224            .factors
225            .prepare(&self.configured_app, component_id)?;
226
227        let store_builder = self.executor.core_engine.store_builder();
228
229        let mut builder = FactorsInstanceBuilder {
230            store_builder,
231            factor_builders,
232            instance_pre,
233            app_component,
234            factors: &self.executor.factors,
235        };
236
237        for hooks in &self.executor.hooks {
238            hooks.prepare_instance(&mut builder)?;
239        }
240
241        Ok(builder)
242    }
243}
244
245/// A FactorsInstanceBuilder manages the instantiation of a Spin component instance.
246///
247/// It is generic over the executor's [`RuntimeFactors`] and any ad-hoc additional
248/// per-instance state needed by the caller.
249pub struct FactorsInstanceBuilder<'a, F: RuntimeFactors, U: 'static> {
250    app_component: AppComponent<'a>,
251    store_builder: spin_core::StoreBuilder,
252    factor_builders: F::InstanceBuilders,
253    instance_pre: &'a InstancePre<F, U>,
254    factors: &'a F,
255}
256
257impl<T: RuntimeFactors, U: 'static> FactorsInstanceBuilder<'_, T, U> {
258    /// Returns the app component for the instance.
259    pub fn app_component(&self) -> &AppComponent<'_> {
260        &self.app_component
261    }
262
263    /// Returns the store builder for the instance.
264    pub fn store_builder(&mut self) -> &mut spin_core::StoreBuilder {
265        &mut self.store_builder
266    }
267
268    /// Returns the factor instance builders for the instance.
269    pub fn factor_builders(&mut self) -> &mut T::InstanceBuilders {
270        &mut self.factor_builders
271    }
272
273    /// Returns the specific instance builder for the given factor.
274    pub fn factor_builder<F: Factor>(&mut self) -> Option<&mut F::InstanceBuilder> {
275        self.factor_builders().for_factor::<F>()
276    }
277
278    /// Returns the underlying wasmtime engine for the instance.
279    pub fn wasmtime_engine(&self) -> &spin_core::WasmtimeEngine {
280        self.instance_pre.engine()
281    }
282
283    /// Returns the compiled component for the instance.
284    pub fn component(&self) -> &Component {
285        self.instance_pre.component()
286    }
287}
288
289impl<T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'_, T, U> {
290    /// Instantiates the instance with the given executor instance state
291    pub async fn instantiate(
292        self,
293        executor_instance_state: U,
294    ) -> anyhow::Result<(
295        spin_core::Instance,
296        spin_core::Store<InstanceState<T::InstanceState, U>>,
297    )> {
298        let instance_state = InstanceState {
299            core: Default::default(),
300            factors: self.factors.build_instance_state(self.factor_builders)?,
301            executor: executor_instance_state,
302            cpu_time_elapsed: Duration::from_millis(0),
303            cpu_time_last_entry: None,
304            memory_used_on_init: 0,
305            component_id: self.app_component.id().into(),
306        };
307        let mut store = self.store_builder.build(instance_state)?;
308
309        #[cfg(feature = "cpu-time-metrics")]
310        store.as_mut().call_hook(|mut store, hook| {
311            CpuTimeCallHook.handle_call_event::<T, U>(store.data_mut(), hook)
312        });
313
314        let instance = self.instance_pre.instantiate_async(&mut store).await?;
315
316        // Track memory usage after instantiation in the instance state.
317        // Note: This only applies if the component has initial memory reservations.
318        store.data_mut().memory_used_on_init = store.data().core_state().memory_consumed();
319
320        Ok((instance, store))
321    }
322
323    pub fn instantiate_store(
324        self,
325        executor_instance_state: U,
326    ) -> anyhow::Result<spin_core::Store<InstanceState<T::InstanceState, U>>> {
327        let instance_state = InstanceState {
328            core: Default::default(),
329            factors: self.factors.build_instance_state(self.factor_builders)?,
330            executor: executor_instance_state,
331            cpu_time_elapsed: Duration::from_millis(0),
332            cpu_time_last_entry: None,
333            memory_used_on_init: 0,
334            component_id: self.app_component.id().into(),
335        };
336        self.store_builder.build(instance_state)
337    }
338}
339
340// Tracks CPU time used by a Wasm guest.
341#[allow(unused)]
342struct CpuTimeCallHook;
343
344#[allow(unused)]
345impl CpuTimeCallHook {
346    fn handle_call_event<T: RuntimeFactors, U>(
347        &self,
348        state: &mut InstanceState<T::InstanceState, U>,
349        ch: CallHook,
350    ) -> wasmtime::Result<()> {
351        match ch {
352            CallHook::CallingWasm | CallHook::ReturningFromHost => {
353                debug_assert!(state.cpu_time_last_entry.is_none());
354                state.cpu_time_last_entry = Some(Instant::now());
355            }
356            CallHook::ReturningFromWasm | CallHook::CallingHost => {
357                let elapsed = state.cpu_time_last_entry.take().unwrap().elapsed();
358                state.cpu_time_elapsed += elapsed;
359            }
360        }
361
362        Ok(())
363    }
364}
365
366/// InstanceState is the [`spin_core::Store`] `data` for an instance.
367///
368/// It is generic over the [`RuntimeFactors::InstanceState`] and any ad-hoc
369/// data needed by the caller.
370pub struct InstanceState<T, U> {
371    core: spin_core::State,
372    factors: T,
373    executor: U,
374    /// The component ID.
375    component_id: String,
376
377    /// The last time guest code started running in this instance.
378    cpu_time_last_entry: Option<Instant>,
379    /// The total CPU time elapsed actively running guest code in this instance.
380    cpu_time_elapsed: Duration,
381    /// The memory (in bytes) consumed on initialization.
382    memory_used_on_init: u64,
383}
384
385impl<T, U> Drop for InstanceState<T, U> {
386    fn drop(&mut self) {
387        // Record the component execution time.
388        #[cfg(feature = "cpu-time-metrics")]
389        spin_telemetry::metrics::histogram_f64!(
390            spin.component_cpu_time = self.cpu_time_elapsed.as_secs_f64(),
391            component_id = self.component_id.clone(),
392            // According to the OpenTelemetry spec, instruments measuring durations should use "s" as the unit.
393            // See https://opentelemetry.io/docs/specs/semconv/general/metrics/#units
394            unit = "s"
395        );
396
397        // Record the component memory consumed on initialization.
398        spin_telemetry::metrics::histogram_u64!(
399            spin.component_memory_used_on_init = self.memory_used_on_init,
400            component_id = self.component_id.clone(),
401            unit = "By"
402        );
403
404        // Record the component memory consumed during execution.
405        spin_telemetry::metrics::histogram_u64!(
406            spin.component_memory_used = self.core.memory_consumed(),
407            component_id = self.component_id.clone(),
408            unit = "By"
409        );
410    }
411}
412
413impl<T, U> InstanceState<T, U> {
414    /// Provides access to the [`spin_core::State`].
415    pub fn core_state(&self) -> &spin_core::State {
416        &self.core
417    }
418
419    /// Provides mutable access to the [`spin_core::State`].
420    pub fn core_state_mut(&mut self) -> &mut spin_core::State {
421        &mut self.core
422    }
423
424    /// Provides access to the [`RuntimeFactors::InstanceState`].
425    pub fn factors_instance_state(&self) -> &T {
426        &self.factors
427    }
428
429    /// Provides mutable access to the [`RuntimeFactors::InstanceState`].
430    pub fn factors_instance_state_mut(&mut self) -> &mut T {
431        &mut self.factors
432    }
433
434    /// Provides access to the ad-hoc executor instance state.
435    pub fn executor_instance_state(&self) -> &U {
436        &self.executor
437    }
438
439    /// Provides mutable access to the ad-hoc executor instance state.
440    pub fn executor_instance_state_mut(&mut self) -> &mut U {
441        &mut self.executor
442    }
443}
444
445impl<T, U> spin_core::AsState for InstanceState<T, U> {
446    fn as_state(&mut self) -> &mut spin_core::State {
447        &mut self.core
448    }
449}
450
451impl<T: RuntimeFactorsInstanceState, U> AsInstanceState<T> for InstanceState<T, U> {
452    fn as_instance_state(&mut self) -> &mut T {
453        &mut self.factors
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use spin_factor_wasi::{DummyFilesMounter, WasiFactor};
460    use spin_factors::RuntimeFactors;
461    use spin_factors_test::TestEnvironment;
462
463    use super::*;
464
465    #[derive(RuntimeFactors)]
466    struct TestFactors {
467        wasi: WasiFactor,
468    }
469
470    #[tokio::test]
471    async fn instance_builder_works() -> anyhow::Result<()> {
472        let factors = TestFactors {
473            wasi: WasiFactor::new(DummyFilesMounter),
474        };
475        let env = TestEnvironment::new(factors);
476        let locked = env.build_locked_app().await?;
477        let app = App::new("test-app", locked);
478
479        let engine_builder = spin_core::Engine::builder(&Default::default())?;
480        let executor = Arc::new(FactorsExecutor::new(engine_builder, env.factors)?);
481
482        let factors_app = executor
483            .load_app(app, Default::default(), &DummyComponentLoader, None, ())
484            .await?;
485
486        let mut instance_builder = factors_app.prepare("empty")?;
487
488        assert_eq!(instance_builder.app_component().id(), "empty");
489
490        instance_builder.store_builder().max_memory_size(1_000_000);
491
492        instance_builder
493            .factor_builder::<WasiFactor>()
494            .unwrap()
495            .args(["foo"]);
496
497        let (_instance, _store) = instance_builder.instantiate(()).await?;
498        Ok(())
499    }
500
501    struct DummyComponentLoader;
502
503    #[async_trait]
504    impl ComponentLoader<TestFactors, ()> for DummyComponentLoader {
505        async fn load_component(
506            &self,
507            engine: &spin_core::wasmtime::Engine,
508            _component: &AppComponent,
509            _trigger_dependencies_composer: &impl TriggerDependenciesComposer,
510        ) -> anyhow::Result<Component> {
511            Ok(Component::new(engine, "(component)")?)
512        }
513    }
514}