use std::{error::Error, fmt::Debug, path::Path, sync::Arc};
use tracing::{debug, 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 enum PoolingConfig {
OnDemand,
Pooling(PoolingOptions),
PoolingWithFallback(PoolingOptions),
}
impl PoolingConfig {
fn strategy(&self) -> wasmtime::InstanceAllocationStrategy {
match self {
PoolingConfig::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand,
PoolingConfig::Pooling(opts) | PoolingConfig::PoolingWithFallback(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
&& enable
{
cfg.memory_protection_keys(wasmtime::Enabled::Auto);
}
wasmtime::InstanceAllocationStrategy::Pooling(cfg)
}
}
}
}
#[derive(Clone, Debug)]
pub struct EngineConfig {
pub pooling_config: PoolingConfig,
pub codegen_cache_dir: Option<Arc<Path>>,
pub consume_fuel: bool,
pub parallel_compilation: bool,
pub debug: bool,
}
impl EngineConfig {
#[cfg(any(test, feature = "test"))]
#[must_use]
pub fn on_demand_testing() -> Self {
let workspace_dir = std::path::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_config: PoolingConfig::OnDemand,
codegen_cache_dir: Some(Arc::from(codegen_cache)),
consume_fuel: false,
parallel_compilation: true,
debug: false,
}
}
#[cfg(test)]
pub(crate) fn pooling_nocache_testing(opts: PoolingOptions) -> Self {
Self {
pooling_config: PoolingConfig::Pooling(opts),
codegen_cache_dir: None,
consume_fuel: false,
parallel_compilation: true,
debug: false,
}
}
}
#[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 dst_wasmtime_config: wasmtime::Config,
config: EngineConfig,
) -> Result<Arc<Engine>, EngineError> {
dst_wasmtime_config.wasm_component_model(true);
dst_wasmtime_config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
dst_wasmtime_config.epoch_interruption(true);
dst_wasmtime_config.consume_fuel(config.consume_fuel);
if config.debug {
dst_wasmtime_config.debug_info(true);
dst_wasmtime_config.cranelift_opt_level(wasmtime::OptLevel::None);
}
dst_wasmtime_config.parallel_compilation(config.parallel_compilation);
dst_wasmtime_config.allocation_strategy(config.pooling_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.as_ref());
let cache =
Cache::new(cache_config).map_err(|err| EngineError::CodegenCache(err.into()))?;
dst_wasmtime_config.cache(Some(cache));
}
Engine::new(&dst_wasmtime_config)
.map(Arc::new)
.map_err(|err| EngineError::Uncategorized(err.into()))
}
pub(crate) fn get_webhook_engine(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
Self::configure_common(wasmtime::Config::new(), config)
}
#[cfg(any(test, feature = "test"))]
pub fn get_activity_engine_test(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
Self::get_activity_engine_internal(config)
}
fn get_activity_engine_internal(config: EngineConfig) -> Result<Arc<Engine>, EngineError> {
Self::configure_common(wasmtime::Config::new(), config)
}
#[cfg(any(test, feature = "test"))]
pub fn get_workflow_engine_test(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.cranelift_nan_canonicalization(true);
wasmtime_config.relaxed_simd_deterministic(true);
Self::configure_common(wasmtime_config, config)
}
pub fn new(engine_config: EngineConfig) -> Result<Self, EngineError> {
let res: Result<_, EngineError> = (|engine_config: &EngineConfig| {
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.clone())?,
})
})(&engine_config);
if let Err(err) = &res
&& matches!(
engine_config.pooling_config,
PoolingConfig::PoolingWithFallback(_)
)
{
warn!("Falling back to on-demand allocator - {err}");
debug!("{err:?}");
let engine_config = EngineConfig {
pooling_config: PoolingConfig::OnDemand,
codegen_cache_dir: engine_config.codegen_cache_dir,
consume_fuel: engine_config.consume_fuel,
parallel_compilation: engine_config.parallel_compilation,
debug: engine_config.debug,
};
Self::new(engine_config)
} else {
res
}
}
}