use std::{error::Error, fmt::Debug, path::PathBuf, sync::Arc};
use tracing::{debug, instrument, warn};
use wasmtime::{Cache, CacheConfig, Engine, EngineWeak, WasmBacktraceDetails};
#[derive(thiserror::Error, Debug)]
pub enum EngineError {
#[error("uncagegorized engine creation error - {0}")]
Uncategorized(Box<dyn Error + Send + Sync>),
#[error("error configuring the codegen cache")]
CodegenCache(Box<dyn Error + Send + Sync>),
}
#[derive(PartialEq, Clone, Default, Debug)]
pub struct PoolingOptions {
pub pooling_memory_keep_resident: Option<usize>,
pub pooling_table_keep_resident: Option<usize>,
pub memory_protection_keys: Option<bool>,
pub pooling_total_core_instances: Option<u32>,
pub pooling_total_component_instances: Option<u32>,
pub pooling_total_memories: Option<u32>,
pub pooling_total_tables: Option<u32>,
pub pooling_total_stacks: Option<u32>,
pub pooling_max_memory_size: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct EngineConfig {
pooling_opts: Option<PoolingOptions>,
codegen_cache_dir: Option<PathBuf>,
}
impl EngineConfig {
fn strategy(&self) -> wasmtime::InstanceAllocationStrategy {
if let Some(opts) = &self.pooling_opts {
let mut cfg = wasmtime::PoolingAllocationConfig::default();
if let Some(size) = opts.pooling_memory_keep_resident {
cfg.linear_memory_keep_resident(size);
}
if let Some(size) = opts.pooling_table_keep_resident {
cfg.table_keep_resident(size);
}
if let Some(limit) = opts.pooling_total_core_instances {
cfg.total_core_instances(limit);
}
if let Some(limit) = opts.pooling_total_component_instances {
cfg.total_component_instances(limit);
}
if let Some(limit) = opts.pooling_total_memories {
cfg.total_memories(limit);
}
if let Some(limit) = opts.pooling_total_tables {
cfg.total_tables(limit);
}
if let Some(limit) = opts.pooling_total_stacks {
cfg.total_stacks(limit);
}
if let Some(limit) = opts.pooling_max_memory_size {
cfg.max_memory_size(limit);
}
if let Some(enable) = opts.memory_protection_keys {
if enable {
cfg.memory_protection_keys(wasmtime::MpkEnabled::Enable);
}
}
wasmtime::InstanceAllocationStrategy::Pooling(cfg)
} else {
wasmtime::InstanceAllocationStrategy::OnDemand
}
}
#[cfg(test)]
#[must_use]
pub fn on_demand_testing() -> Self {
let workspace_dir = PathBuf::from(
std::env::var("CARGO_WORKSPACE_DIR").expect("CARGO_WORKSPACE_DIR must be set"),
);
let codegen_cache = workspace_dir.join("test-codegen-cache");
Self {
pooling_opts: None,
codegen_cache_dir: Some(codegen_cache),
}
}
#[cfg(test)]
pub(crate) fn pooling_nocache_testing(opts: PoolingOptions) -> Self {
Self {
pooling_opts: Some(opts),
codegen_cache_dir: None,
}
}
}
#[derive(Clone)]
pub struct Engines {
pub activity_engine: Arc<Engine>,
pub webhook_engine: Arc<Engine>,
pub workflow_engine: Arc<Engine>,
}
impl Engines {
#[must_use]
pub fn weak_refs(&self) -> Vec<EngineWeak> {
vec![
self.activity_engine.weak(),
self.workflow_engine.weak(),
self.workflow_engine.weak(),
]
}
fn configure_common(
mut wasmtime_config: wasmtime::Config,
config: EngineConfig,
) -> Result<Arc<Engine>, EngineError> {
wasmtime_config.wasm_component_model(true);
wasmtime_config.async_support(true);
wasmtime_config.allocation_strategy(config.strategy());
if let Some(codegen_cache_dir) = config.codegen_cache_dir {
let mut cache_config = CacheConfig::new();
cache_config.with_directory(codegen_cache_dir);
let cache =
Cache::new(cache_config).map_err(|err| EngineError::CodegenCache(err.into()))?;
wasmtime_config.cache(Some(cache));
}
Engine::new(&wasmtime_config)
.map(Arc::new)
.map_err(|err| EngineError::Uncategorized(err.into()))
}
pub(crate) fn get_webhook_engine(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
let mut wasmtime_config = wasmtime::Config::new();
wasmtime_config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
wasmtime_config.epoch_interruption(true);
Self::configure_common(wasmtime_config, config)
}
#[cfg(test)]
pub fn get_activity_engine(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
Self::get_activity_engine_internal(config)
}
fn get_activity_engine_internal(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
let mut wasmtime_config = wasmtime::Config::new();
wasmtime_config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
wasmtime_config.epoch_interruption(true);
Self::configure_common(wasmtime_config, config)
}
#[cfg(test)]
pub fn get_workflow_engine(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
Self::get_workflow_engine_internal(config)
}
fn get_workflow_engine_internal(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
let mut wasmtime_config = wasmtime::Config::new();
wasmtime_config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
wasmtime_config.epoch_interruption(true);
wasmtime_config.cranelift_nan_canonicalization(true);
wasmtime_config.relaxed_simd_deterministic(true);
Self::configure_common(wasmtime_config, config)
}
#[instrument(skip_all)]
pub fn on_demand(codegen_cache_dir: Option<PathBuf>) -> Result<Self, EngineError> {
let engine_config = EngineConfig {
pooling_opts: None,
codegen_cache_dir,
};
Ok(Engines {
activity_engine: Self::get_activity_engine_internal(engine_config.clone())?,
webhook_engine: Self::get_webhook_engine(engine_config.clone())?,
workflow_engine: Self::get_workflow_engine_internal(engine_config)?,
})
}
#[instrument(skip_all)]
pub fn pooling(
opts: PoolingOptions,
codegen_cache_dir: Option<PathBuf>,
) -> Result<Self, EngineError> {
let engine_config = EngineConfig {
pooling_opts: Some(opts),
codegen_cache_dir,
};
Ok(Engines {
activity_engine: Self::get_activity_engine_internal(engine_config.clone())?,
webhook_engine: Self::get_webhook_engine(engine_config.clone())?,
workflow_engine: Self::get_workflow_engine_internal(engine_config)?,
})
}
pub fn auto_detect_allocator(
pooling_opts: PoolingOptions,
codegen_cache_dir: Option<PathBuf>,
) -> Result<Self, EngineError> {
Self::pooling(pooling_opts, codegen_cache_dir.clone()).or_else(|err| {
warn!("Falling back to on-demand allocator - {err}");
debug!("{err:?}");
Self::on_demand(codegen_cache_dir)
})
}
}