use crate::ipc;
use crate::scheduler::Cancellation;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
const FUEL_PER_MILLISECOND: u64 = 1_000_000;
const MAX_FUEL: u64 = 10_000_000_000;
const EPOCH_INTERVAL: Duration = Duration::from_millis(10);
pub struct ExecutionBudget {
pub fuel: u64,
pub deadline: Instant,
pub cancellation: Arc<Cancellation>,
authority: Option<Box<dyn Fn() -> Result<(), WorkerFailure> + Send + Sync>>,
}
impl ExecutionBudget {
pub fn new(remaining: Duration, cancellation: Arc<Cancellation>) -> Self {
let milliseconds = remaining.as_millis().min(u128::from(u64::MAX)) as u64;
Self {
fuel: milliseconds
.saturating_mul(FUEL_PER_MILLISECOND)
.min(MAX_FUEL),
deadline: Instant::now() + remaining,
cancellation,
authority: None,
}
}
pub fn with_authority(
mut self,
check: impl Fn() -> Result<(), WorkerFailure> + Send + Sync + 'static,
) -> Self {
self.authority = Some(Box::new(check));
self
}
pub fn check(&self) -> Result<(), WorkerFailure> {
if self.cancellation.is_canceled() {
return Err(WorkerFailure::Canceled);
}
if Instant::now() >= self.deadline {
return Err(WorkerFailure::Timeout);
}
if let Some(check) = &self.authority {
check()?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum WorkerFailure {
Invalid(String),
Authority { code: &'static str, message: String },
Budget,
Memory,
Timeout,
Canceled,
}
impl WorkerFailure {
pub fn code(&self) -> &'static str {
match self {
Self::Invalid(_) => ipc::ERR_WASM_WORKER_INVALID,
Self::Authority { code, .. } => code,
Self::Budget => ipc::ERR_WASM_WORKER_BUDGET_EXCEEDED,
Self::Memory => ipc::ERR_WASM_WORKER_MEMORY_LIMIT,
Self::Timeout => ipc::ERR_RUNTIME_INVOCATION_TIMEOUT,
Self::Canceled => ipc::ERR_RUNTIME_INVOCATION_CANCELED,
}
}
pub fn engine(error: wasmtime::Error) -> Self {
if let Some(failure) = error.downcast_ref::<Self>() {
return match failure {
Self::Invalid(message) => Self::Invalid(message.clone()),
Self::Authority { code, message } => Self::Authority {
code,
message: message.clone(),
},
Self::Budget => Self::Budget,
Self::Memory => Self::Memory,
Self::Timeout => Self::Timeout,
Self::Canceled => Self::Canceled,
};
}
if error.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::OutOfFuel) {
return Self::Budget;
}
Self::Invalid(format!("{error:#}"))
}
}
impl From<String> for WorkerFailure {
fn from(message: String) -> Self {
Self::Invalid(message)
}
}
impl std::fmt::Display for WorkerFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Invalid(message) | Self::Authority { message, .. } => f.write_str(message),
Self::Budget => f.write_str("worker computation budget exhausted"),
Self::Memory => f.write_str("worker memory or table limit exceeded"),
Self::Timeout => f.write_str("worker execution deadline exceeded"),
Self::Canceled => f.write_str("runtime invocation was canceled"),
}
}
}
impl std::error::Error for WorkerFailure {}
pub struct EpochTicker {
stop: Arc<(Mutex<bool>, Condvar)>,
thread: Option<JoinHandle<()>>,
}
impl EpochTicker {
pub fn new(engine: wasmtime::Engine) -> Self {
let stop = Arc::new((Mutex::new(false), Condvar::new()));
let signal = Arc::clone(&stop);
let thread = thread::Builder::new()
.name("redevplugin-epoch".into())
.spawn(move || {
let (lock, wake) = &*signal;
let mut stopped = lock.lock().expect("epoch timer mutex poisoned");
while !*stopped {
let result = wake
.wait_timeout(stopped, EPOCH_INTERVAL)
.expect("epoch timer wait poisoned");
stopped = result.0;
if !*stopped {
engine.increment_epoch();
}
}
})
.expect("start bounded runtime epoch timer");
Self {
stop,
thread: Some(thread),
}
}
}
impl Drop for EpochTicker {
fn drop(&mut self) {
*self.stop.0.lock().expect("epoch timer mutex poisoned") = true;
self.stop.1.notify_all();
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
pub struct WorkerLimits(pub wasmtime::StoreLimits);
impl wasmtime::ResourceLimiter for WorkerLimits {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> wasmtime::Result<bool> {
self.0
.memory_growing(current, desired, maximum)
.map_err(|_| wasmtime::Error::new(WorkerFailure::Memory))
}
fn table_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> wasmtime::Result<bool> {
self.0
.table_growing(current, desired, maximum)
.map_err(|_| wasmtime::Error::new(WorkerFailure::Memory))
}
fn memory_grow_failed(&mut self, _: wasmtime::Error) -> wasmtime::Result<()> {
Err(wasmtime::Error::new(WorkerFailure::Memory))
}
fn table_grow_failed(&mut self, _: wasmtime::Error) -> wasmtime::Result<()> {
Err(wasmtime::Error::new(WorkerFailure::Memory))
}
fn instances(&self) -> usize {
1
}
fn tables(&self) -> usize {
1
}
fn memories(&self) -> usize {
1
}
}
#[cfg(test)]
mod contract_tests {
use super::*;
#[test]
fn execution_policy_matches_canonical_contract() {
let contract: serde_json::Value =
serde_json::from_str(include_str!("../testdata/execution-policy.json")).unwrap();
assert_eq!(contract["engine"], "wasmtime-50.0.1");
assert_eq!(
contract["fuel_per_remaining_millisecond"],
FUEL_PER_MILLISECOND
);
assert_eq!(contract["maximum_fuel"], MAX_FUEL);
assert_eq!(
contract["epoch_interval_milliseconds"],
EPOCH_INTERVAL.as_millis() as u64
);
for (key, failure) in [
("fuel", WorkerFailure::Budget),
("memory", WorkerFailure::Memory),
("deadline", WorkerFailure::Timeout),
("cancel", WorkerFailure::Canceled),
] {
assert_eq!(contract["errors"][key], failure.code());
}
}
}