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