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}