wasmcloud_runtime/
runtime.rs

1use crate::{experimental::Features, ComponentConfig};
2
3use core::fmt;
4use core::fmt::Debug;
5use core::time::Duration;
6
7use std::thread;
8
9use anyhow::Context;
10use wasmtime::{InstanceAllocationStrategy, PoolingAllocationConfig};
11
12/// Default max linear memory for a component (256 MiB)
13pub const MAX_LINEAR_MEMORY: u64 = 256 * 1024 * 1024;
14/// Default max component size (50 MiB)
15pub const MAX_COMPONENT_SIZE: u64 = 50 * 1024 * 1024;
16/// Default max number of components
17pub const MAX_COMPONENTS: u32 = 10_000;
18
19/// [`RuntimeBuilder`] used to configure and build a [Runtime]
20#[derive(Clone, Default)]
21pub struct RuntimeBuilder {
22    engine_config: wasmtime::Config,
23    max_components: u32,
24    max_component_size: u64,
25    max_linear_memory: u64,
26    max_execution_time: Duration,
27    component_config: ComponentConfig,
28    force_pooling_allocator: bool,
29    experimental_features: Features,
30}
31
32impl RuntimeBuilder {
33    /// Returns a new [`RuntimeBuilder`]
34    #[must_use]
35    pub fn new() -> Self {
36        let mut engine_config = wasmtime::Config::default();
37        engine_config.async_support(true);
38        engine_config.epoch_interruption(true);
39        engine_config.wasm_component_model(true);
40
41        Self {
42            engine_config,
43            max_components: MAX_COMPONENTS,
44            // Why so large you ask? Well, python components are chonky, like 35MB for a hello world
45            // chonky. So this is pretty big for now.
46            max_component_size: MAX_COMPONENT_SIZE,
47            max_linear_memory: MAX_LINEAR_MEMORY,
48            max_execution_time: Duration::from_secs(10 * 60),
49            component_config: ComponentConfig::default(),
50            force_pooling_allocator: false,
51            experimental_features: Features::default(),
52        }
53    }
54
55    /// Set a custom [`ComponentConfig`] to use for all component instances
56    #[must_use]
57    pub fn component_config(self, component_config: ComponentConfig) -> Self {
58        Self {
59            component_config,
60            ..self
61        }
62    }
63
64    /// Sets the maximum number of components that can be run simultaneously. Defaults to 10000
65    #[must_use]
66    pub fn max_components(self, max_components: u32) -> Self {
67        Self {
68            max_components,
69            ..self
70        }
71    }
72
73    /// Sets the maximum size of a component instance, in bytes. Defaults to 50MB
74    #[must_use]
75    pub fn max_component_size(self, max_component_size: u64) -> Self {
76        Self {
77            max_component_size,
78            ..self
79        }
80    }
81
82    /// Sets the maximum amount of linear memory that can be used by all components. Defaults to 10MB
83    #[must_use]
84    pub fn max_linear_memory(self, max_linear_memory: u64) -> Self {
85        Self {
86            max_linear_memory,
87            ..self
88        }
89    }
90
91    /// Sets the maximum execution time of a component. Defaults to 10 minutes.
92    /// This operates on second precision and value of 1 second is the minimum.
93    /// Any value below 1 second will be interpreted as 1 second limit.
94    #[must_use]
95    pub fn max_execution_time(self, max_execution_time: Duration) -> Self {
96        Self {
97            max_execution_time: max_execution_time.max(Duration::from_secs(1)),
98            ..self
99        }
100    }
101
102    /// Forces the use of the pooling allocator. This may cause the runtime to fail if there isn't enough memory for the pooling allocator
103    #[must_use]
104    pub fn force_pooling_allocator(self) -> Self {
105        Self {
106            force_pooling_allocator: true,
107            ..self
108        }
109    }
110
111    /// Set the experimental features to enable in the runtime
112    #[must_use]
113    pub fn experimental_features(self, experimental_features: Features) -> Self {
114        Self {
115            experimental_features,
116            ..self
117        }
118    }
119
120    /// Turns this builder into a [`Runtime`]
121    ///
122    /// # Errors
123    ///
124    /// Fails if the configuration is not valid
125    #[allow(clippy::type_complexity)]
126    pub fn build(mut self) -> anyhow::Result<(Runtime, thread::JoinHandle<Result<(), ()>>)> {
127        let mut pooling_config = PoolingAllocationConfig::default();
128
129        // Right now we assume tables_per_component is the same as memories_per_component just like
130        // the default settings (which has a 1:1 relationship between total memories and total
131        // tables), but we may want to change that later. I would love to figure out a way to
132        // configure all these values via something smarter that can look at total memory available
133        let memories_per_component = 1;
134        let tables_per_component = 1;
135        let max_core_instances_per_component = 30;
136        let table_elements = 15000;
137
138        #[allow(clippy::cast_possible_truncation)]
139        pooling_config
140            .total_component_instances(self.max_components)
141            .total_core_instances(self.max_components)
142            .total_gc_heaps(self.max_components)
143            .total_stacks(self.max_components)
144            .max_component_instance_size(self.max_component_size as usize)
145            .max_core_instances_per_component(max_core_instances_per_component)
146            .max_tables_per_component(20)
147            .table_elements(table_elements)
148            // The number of memories an instance can have effectively limits the number of inner components
149            // a composed component can have (since each inner component has its own memory). We default to 32 for now, and
150            // we'll see how often this limit gets reached.
151            .max_memories_per_component(max_core_instances_per_component * memories_per_component)
152            .total_memories(self.max_components * memories_per_component)
153            .total_tables(self.max_components * tables_per_component)
154            // Restrict the maximum amount of linear memory that can be used by a component,
155            // which influences two things we care about:
156            //
157            // - How large of a component we can load (i.e. all components must be less than this value)
158            // - How much memory a fully loaded host carrying c components will use
159            .max_memory_size(self.max_linear_memory as usize)
160            // These numbers are set to avoid page faults when trying to claim new space on linux
161            .linear_memory_keep_resident(10 * 1024)
162            .table_keep_resident(10 * 1024);
163        self.engine_config
164            .allocation_strategy(InstanceAllocationStrategy::Pooling(pooling_config));
165        let engine = match wasmtime::Engine::new(&self.engine_config)
166            .context("failed to construct engine")
167        {
168            Ok(engine) => engine,
169            Err(e) if self.force_pooling_allocator => {
170                anyhow::bail!("failed to construct engine with pooling allocator: {}", e)
171            }
172            Err(e) => {
173                tracing::warn!(err = %e, "failed to construct engine with pooling allocator, falling back to dynamic allocator which may result in slower startup and execution of components.");
174                self.engine_config
175                    .allocation_strategy(InstanceAllocationStrategy::OnDemand);
176                wasmtime::Engine::new(&self.engine_config).context("failed to construct engine")?
177            }
178        };
179        let epoch = {
180            let engine = engine.weak();
181            thread::spawn(move || loop {
182                thread::sleep(Duration::from_secs(1));
183                let Some(engine) = engine.upgrade() else {
184                    return Ok(());
185                };
186                engine.increment_epoch();
187            })
188        };
189        Ok((
190            Runtime {
191                engine,
192                component_config: self.component_config,
193                max_execution_time: self.max_execution_time,
194                experimental_features: self.experimental_features,
195            },
196            epoch,
197        ))
198    }
199}
200
201impl TryFrom<RuntimeBuilder> for (Runtime, thread::JoinHandle<Result<(), ()>>) {
202    type Error = anyhow::Error;
203
204    fn try_from(builder: RuntimeBuilder) -> Result<Self, Self::Error> {
205        builder.build()
206    }
207}
208
209/// Shared wasmCloud runtime
210#[derive(Clone)]
211pub struct Runtime {
212    pub(crate) engine: wasmtime::Engine,
213    pub(crate) component_config: ComponentConfig,
214    pub(crate) max_execution_time: Duration,
215    pub(crate) experimental_features: Features,
216}
217
218impl Debug for Runtime {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        f.debug_struct("Runtime")
221            .field("component_config", &self.component_config)
222            .field("runtime", &"wasmtime")
223            .field("max_execution_time", &"max_execution_time")
224            .finish_non_exhaustive()
225    }
226}
227
228impl Runtime {
229    /// Returns a new [`Runtime`] configured with defaults
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the default configuration is invalid
234    #[allow(clippy::type_complexity)]
235    pub fn new() -> anyhow::Result<(Self, thread::JoinHandle<Result<(), ()>>)> {
236        Self::builder().try_into()
237    }
238
239    /// Returns a new [`RuntimeBuilder`], which can be used to configure and build a [Runtime]
240    #[must_use]
241    pub fn builder() -> RuntimeBuilder {
242        RuntimeBuilder::new()
243    }
244
245    /// [Runtime] version
246    #[must_use]
247    pub fn version(&self) -> &'static str {
248        env!("CARGO_PKG_VERSION")
249    }
250
251    /// Returns a boolean indicating whether the runtime should skip linking a feature-gated instance
252    pub(crate) fn skip_feature_gated_instance(&self, instance: &str) -> bool {
253        match instance {
254            "wasmcloud:messaging/producer@0.3.0"
255            | "wasmcloud:messaging/request-reply@0.3.0"
256            | "wasmcloud:messaging/types@0.3.0"
257                if self.experimental_features.wasmcloud_messaging_v3 =>
258            {
259                true
260            }
261            "wasmcloud:identity/store@0.0.1"
262                if self.experimental_features.workload_identity_interface =>
263            {
264                true
265            }
266            _ => false,
267        }
268    }
269}