Skip to main content

wash_runtime/engine/
mod.rs

1//! WebAssembly component engine for executing workloads.
2//!
3//! This module provides the core engine functionality for compiling and executing
4//! WebAssembly components. The [`Engine`] is responsible for:
5//!
6//! - Compiling WebAssembly components using wasmtime
7//! - Initializing workloads with their components and dependencies
8//! - Managing volume mounts and resource configurations
9//! - Setting up WASI and HTTP interfaces for components
10//!
11//! # Key Types
12//!
13//! - [`Engine`] - The main engine for WebAssembly execution
14//! - [`EngineBuilder`] - Builder for configuring engine settings
15//! - [`WorkloadComponent`] - Individual components within a workload
16//!
17//! # Example
18//!
19//! ```no_run
20//! use wash_runtime::engine::Engine;
21//! use wash_runtime::types::Workload;
22//!
23//! # async fn example() -> anyhow::Result<()> {
24//! let engine = Engine::builder().build()?;
25//! let workload = Workload {
26//!     namespace: "default".to_string(),
27//!     name: "my-workload".to_string(),
28//!     // ... other fields
29//! #   annotations: std::collections::HashMap::new(),
30//! #   service: None,
31//! #   components: vec![],
32//! #   host_interfaces: vec![],
33//! #   volumes: vec![],
34//! };
35//!
36//! let unresolved = engine.initialize_workload("workload-1", workload)?;
37//! // ... bind to plugins and resolve
38//! # Ok(())
39//! # }
40//! ```
41
42use anyhow::{Context, bail};
43use wasmtime::PoolingAllocationConfig;
44use wasmtime::component::{Component, Linker};
45
46use crate::engine::ctx::Ctx;
47use crate::engine::workload::{UnresolvedWorkload, WorkloadComponent, WorkloadService};
48use crate::types::{EmptyDirVolume, HostPathVolume, VolumeType, Workload};
49use std::path::PathBuf;
50
51pub mod ctx;
52mod value;
53pub mod workload;
54
55/// The core WebAssembly engine for executing components and workloads.
56///
57/// The `Engine` is responsible for compiling WebAssembly components, managing
58/// their lifecycle, and providing the runtime environment for execution.
59/// It wraps a wasmtime engine with additional functionality for workload management.
60#[derive(Debug, Clone)]
61pub struct Engine {
62    // wasmtime engine
63    pub(crate) inner: wasmtime::Engine,
64}
65
66impl Engine {
67    /// Creates a new [`EngineBuilder`] for configuring an engine.
68    ///
69    /// # Returns
70    /// A default `EngineBuilder` that can be customized with additional configuration.
71    pub fn builder() -> EngineBuilder {
72        EngineBuilder::default()
73    }
74
75    /// Gets a reference to the inner wasmtime engine.
76    ///
77    /// This provides access to the underlying wasmtime engine for advanced use cases.
78    ///
79    /// # Returns
80    /// A reference to the internal `wasmtime::Engine`.
81    pub fn inner(&self) -> &wasmtime::Engine {
82        &self.inner
83    }
84
85    /// Initializes a workload by validating and preparing all its components.
86    ///
87    /// This function takes a workload definition and prepares it for execution by:
88    /// - Validating service components (if present)
89    /// - Setting up volumes (both host path and empty directory types)
90    /// - Initializing all components with their resource configurations
91    ///
92    /// # Arguments
93    /// * `id` - Unique identifier for this workload instance
94    /// * `workload` - The workload configuration containing components, services, and volumes
95    ///
96    /// # Returns
97    /// An `UnresolvedWorkload` that still needs to be bound to plugins and resolved
98    /// before execution.
99    ///
100    /// # Errors
101    /// Returns an error if:
102    /// - Service component validation fails
103    /// - Volume paths don't exist or aren't accessible
104    /// - Component initialization fails
105    pub fn initialize_workload(
106        &self,
107        id: impl AsRef<str>,
108        workload: Workload,
109    ) -> anyhow::Result<UnresolvedWorkload> {
110        let Workload {
111            namespace,
112            name,
113            components,
114            service,
115            volumes,
116            host_interfaces,
117            ..
118        } = workload;
119
120        // Process and validate volumes - create a lookup map from volume name to validated host path
121        let mut validated_volumes = std::collections::HashMap::new();
122
123        for v in volumes {
124            let host_path = match v.volume_type {
125                VolumeType::HostPath(HostPathVolume { local_path }) => {
126                    let path = PathBuf::from(&local_path);
127                    if !path.is_dir() {
128                        anyhow::bail!(
129                            "HostPath volume '{local_path}' does not exist or is not a directory",
130                        );
131                    }
132                    path
133                }
134                VolumeType::EmptyDir(EmptyDirVolume {}) => {
135                    // Create a temporary directory for the empty dir volume
136                    let temp_dir = tempfile::tempdir()
137                        .context("failed to create temp dir for empty dir volume")?;
138                    tracing::debug!(path = ?temp_dir.path(), "created temp dir for empty dir volume");
139                    temp_dir.keep()
140                }
141            };
142
143            // Store the validated volume for later lookup
144            validated_volumes.insert(v.name.clone(), host_path);
145        }
146
147        // Iniitalize service
148        let service = if let Some(svc) = service {
149            match self.initialize_service(id.as_ref(), &name, &namespace, svc, &validated_volumes) {
150                Ok(handle) => {
151                    tracing::debug!("successfully initialized service component");
152                    Some(handle)
153                }
154                Err(e) => {
155                    tracing::error!(err = ?e, "failed to initialize service component");
156                    bail!(e);
157                }
158            }
159        } else {
160            None
161        };
162
163        // Initialize all components
164        let mut workload_components = Vec::new();
165        for component in components.into_iter() {
166            match self.initialize_workload_component(
167                id.as_ref(),
168                &name,
169                &namespace,
170                component,
171                &validated_volumes,
172            ) {
173                Ok(handle) => {
174                    tracing::debug!("successfully initialized workload component");
175                    workload_components.push(handle);
176                }
177                Err(e) => {
178                    tracing::error!(err = ?e, "failed to initialize component");
179                    bail!(e);
180                }
181            }
182        }
183
184        Ok(UnresolvedWorkload::new(
185            id.as_ref(),
186            name,
187            namespace,
188            service,
189            workload_components,
190            host_interfaces,
191        ))
192    }
193
194    fn initialize_service(
195        &self,
196        workload_id: impl AsRef<str>,
197        workload_name: impl AsRef<str>,
198        workload_namespace: impl AsRef<str>,
199        service: crate::types::Service,
200        validated_volumes: &std::collections::HashMap<String, PathBuf>,
201    ) -> anyhow::Result<WorkloadService> {
202        // Create a wasmtime component from the bytes
203        let wasmtime_component = Component::new(&self.inner, service.bytes)
204            .context("failed to create component from bytes")?;
205
206        // Create a linker for this component
207        let mut linker: Linker<Ctx> = Linker::new(&self.inner);
208
209        // Add WASI@0.2 interfaces to the linker
210        wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to add WASI to linker")?;
211
212        // TODO: only if workload declares incoming-handler or outgoing-handler & the component export/imports the interfaces
213        // Add HTTP interfaces to the linker
214        #[cfg(feature = "wasi-http")]
215        wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
216            .context("failed to add wasi:http/types to linker")?;
217
218        // Build volume mounts for this component by looking up validated volumes
219        let mut component_volume_mounts = Vec::new();
220        for vm in &service.local_resources.volume_mounts {
221            if let Some(host_path) = validated_volumes.get(&vm.name) {
222                component_volume_mounts.push((host_path.clone(), vm.clone()));
223            } else {
224                tracing::warn!(
225                    volume = %vm.name,
226                    "component references volume that was not found in workload volumes",
227                );
228            }
229        }
230
231        // Create the WorkloadService with volume mounts
232        Ok(WorkloadService::new(
233            workload_id.as_ref(),
234            workload_name.as_ref(),
235            workload_namespace.as_ref(),
236            wasmtime_component,
237            linker,
238            component_volume_mounts,
239            service.local_resources,
240            service.max_restarts,
241        ))
242    }
243
244    /// Initialize a component that is a part of a workload, add wasi@0.2 interfaces (and
245    /// wasi:http if the `http` feature is enabled) to the linker.
246    fn initialize_workload_component(
247        &self,
248        workload_id: impl AsRef<str>,
249        workload_name: impl AsRef<str>,
250        workload_namespace: impl AsRef<str>,
251        component: crate::types::Component,
252        validated_volumes: &std::collections::HashMap<String, PathBuf>,
253    ) -> anyhow::Result<WorkloadComponent> {
254        // Create a wasmtime component from the bytes
255        let wasmtime_component = Component::new(&self.inner, component.bytes)
256            .context("failed to create component from bytes")?;
257
258        // Create a linker for this component
259        let mut linker: Linker<Ctx> = Linker::new(&self.inner);
260
261        // Add WASI@0.2 interfaces to the linker
262        wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to add WASI to linker")?;
263
264        // TODO: only if workload declares incoming-handler or outgoing-handler & the component export/imports the interfaces
265        // Add HTTP interfaces to the linker
266        #[cfg(feature = "wasi-http")]
267        wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
268            .context("failed to add wasi:http/types to linker")?;
269
270        // Build volume mounts for this component by looking up validated volumes
271        let mut component_volume_mounts = Vec::new();
272        for vm in &component.local_resources.volume_mounts {
273            if let Some(host_path) = validated_volumes.get(&vm.name) {
274                component_volume_mounts.push((host_path.clone(), vm.clone()));
275            } else {
276                tracing::warn!(
277                    volume = %vm.name,
278                    "component references volume that was not found in workload volumes",
279                );
280            }
281        }
282
283        // Create the WorkloadComponent with volume mounts
284        Ok(WorkloadComponent::new(
285            workload_id.as_ref(),
286            workload_name.as_ref(),
287            workload_namespace.as_ref(),
288            wasmtime_component,
289            linker,
290            component_volume_mounts,
291            component.local_resources,
292            // TODO: implement pooling and instance limits
293            // component.pool_size,
294            // component.max_invocations,
295        ))
296    }
297}
298
299/// Builder for constructing an [`Engine`] with custom configuration.
300///
301/// The builder pattern allows for flexible configuration of the engine
302/// before creation. By default, it enables async support which is required
303/// for component execution.
304#[derive(Default)]
305pub struct EngineBuilder {
306    config: wasmtime::Config,
307    use_pooling_allocator: Option<bool>,
308}
309
310impl EngineBuilder {
311    /// Creates a new `EngineBuilder` with default configuration.
312    ///
313    /// # Returns
314    /// A new builder instance with default wasmtime configuration.
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    /// Enables or disables the pooling allocator for instance allocation.
320    pub fn with_pooling_allocator(mut self, enable: bool) -> Self {
321        self.use_pooling_allocator = Some(enable);
322        self
323    }
324
325    /// Sets a custom wasmtime configuration for the engine.
326    ///
327    /// This allows full control over the wasmtime engine configuration,
328    /// including compilation settings, runtime limits, and feature flags.
329    ///
330    /// # Arguments
331    /// * `config` - A wasmtime `Config` object with custom settings
332    ///
333    /// # Returns
334    /// The builder instance for method chaining.
335    pub fn with_config(mut self, config: wasmtime::Config) -> Self {
336        self.config = config;
337        self
338    }
339}
340
341impl EngineBuilder {
342    /// Builds and returns a configured [`Engine`].
343    ///
344    /// This method finalizes the configuration and creates the engine.
345    /// It automatically enables async support which is required for
346    /// component execution.
347    ///
348    /// # Returns
349    /// A new `Engine` instance configured with the builder's settings.
350    ///
351    /// # Errors
352    /// Returns an error if the wasmtime engine creation fails.
353    pub fn build(mut self) -> anyhow::Result<Engine> {
354        // Async support must be enabled
355        self.config.async_support(true);
356        // The pooling allocator can be more efficient for workloads with many short-lived instances
357        if let Ok(true) = use_pooling_allocator_by_default(self.use_pooling_allocator) {
358            tracing::debug!("using pooling allocator by default");
359            self.config
360                .allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(
361                    PoolingAllocationConfig::default(),
362                ));
363        }
364
365        let inner = wasmtime::Engine::new(&self.config)?;
366        Ok(Engine { inner })
367    }
368}
369
370// TL;DR this is likely best for machines that can handle the large virtual memory requirement of the pooling allocator
371// https://github.com/bytecodealliance/wasmtime/blob/b943666650696f1eb7ff8b217762b58d5ef5779d/src/commands/serve.rs#L641-L656
372fn use_pooling_allocator_by_default(enable: Option<bool>) -> anyhow::Result<bool> {
373    const BITS_TO_TEST: u32 = 42;
374    if let Some(v) = enable {
375        return Ok(v);
376    }
377    let mut config = wasmtime::Config::new();
378    config.wasm_memory64(true);
379    config.memory_reservation(1 << BITS_TO_TEST);
380    let engine = wasmtime::Engine::new(&config)?;
381    let mut store = wasmtime::Store::new(&engine, ());
382    // NB: the maximum size is in wasm pages to take out the 16-bits of wasm
383    // page size here from the maximum size.
384    let ty = wasmtime::MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16)));
385    Ok(wasmtime::Memory::new(&mut store, ty).is_ok())
386}