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