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