use super::error::{
WasmtimePluginRuntimeError, WasmtimePluginRuntimeFailure, WasmtimePluginRuntimeFailureKind,
};
use crate::plugin::{PluginIdentity, PluginOperationalExport};
use std::{
fmt::{Debug, Formatter},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use wasmtime::{Engine, Error as WasmtimeError, Trap};
#[derive(Clone)]
pub(super) struct EpochTicker {
inner: Arc<EpochTickerInner>,
}
impl EpochTicker {
pub(super) fn start(engine: Engine, interval: Duration) -> std::io::Result<Self> {
let active = Arc::new(AtomicBool::new(true));
let worker_active = Arc::clone(&active);
let handle = thread::Builder::new()
.name(String::from("alma-plugin-epoch"))
.spawn(move || {
while worker_active.load(Ordering::Acquire) {
thread::sleep(interval);
engine.increment_epoch();
}
})?;
Ok(Self {
inner: Arc::new(EpochTickerInner {
active,
handle: Mutex::new(Some(handle)),
interval,
}),
})
}
pub(super) fn interval(&self) -> Duration {
self.inner.interval
}
}
impl Debug for EpochTicker {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("EpochTicker")
.field("interval", &self.inner.interval)
.finish_non_exhaustive()
}
}
pub(super) struct EpochTickerInner {
active: Arc<AtomicBool>,
handle: Mutex<Option<JoinHandle<()>>>,
interval: Duration,
}
impl Drop for EpochTickerInner {
fn drop(&mut self) {
self.active.store(false, Ordering::Release);
if let Ok(mut handle) = self.handle.lock()
&& let Some(handle) = handle.take()
{
let _joined = handle.join();
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct GuestDeadline {
pub(super) started: Instant,
pub(super) timeout: Duration,
pub(super) epoch_ticks: u64,
}
impl GuestDeadline {
pub(super) fn new(timeout: Duration, ticker: &EpochTicker) -> Self {
Self {
started: Instant::now(),
timeout,
epoch_ticks: epoch_ticks_for(timeout, ticker.interval()),
}
}
pub(super) const fn epoch_ticks(self) -> u64 {
self.epoch_ticks
}
pub(super) fn expired(self) -> bool {
self.started.elapsed() >= self.timeout
}
pub(super) fn trapped_by_deadline(self, source: &WasmtimeError) -> bool {
matches!(source.downcast_ref::<Trap>(), Some(Trap::Interrupt)) || self.expired()
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct GuestExecutionGuard {
pub(super) export: PluginOperationalExport,
pub(super) timeout_ms: u64,
pub(super) deadline: GuestDeadline,
}
impl GuestExecutionGuard {
pub(super) fn new(
export: PluginOperationalExport,
timeout_ms: u64,
ticker: &EpochTicker,
) -> Self {
let timeout = Duration::from_millis(timeout_ms);
Self {
export,
timeout_ms,
deadline: GuestDeadline::new(timeout, ticker),
}
}
pub(super) const fn epoch_ticks(self) -> u64 {
self.deadline.epoch_ticks()
}
pub(super) fn ensure_result(
self,
identity: &PluginIdentity,
result: Result<(), WasmtimeError>,
failure_kind: WasmtimePluginRuntimeFailureKind,
) -> Result<(), WasmtimePluginRuntimeError> {
result.map_err(|source| self.runtime_error(identity, &source, failure_kind))
}
pub(super) fn ensure_not_expired(
self,
identity: &PluginIdentity,
) -> Result<(), WasmtimePluginRuntimeError> {
if self.deadline.expired() {
return Err(self.timeout_error(identity));
}
Ok(())
}
pub(super) fn runtime_error(
self,
identity: &PluginIdentity,
source: &WasmtimeError,
failure_kind: WasmtimePluginRuntimeFailureKind,
) -> WasmtimePluginRuntimeError {
if self.deadline.trapped_by_deadline(source) {
return self.timeout_error(identity);
}
WasmtimePluginRuntimeError::GuestTrap {
identity: identity.clone(),
export: self.export,
failure: WasmtimePluginRuntimeFailure::from_wasmtime(failure_kind, &source),
}
}
pub(super) fn timeout_error(self, identity: &PluginIdentity) -> WasmtimePluginRuntimeError {
WasmtimePluginRuntimeError::Timeout {
identity: identity.clone(),
export: self.export,
timeout_ms: self.timeout_ms,
}
}
}
pub(super) fn epoch_ticks_for(timeout: Duration, interval: Duration) -> u64 {
let timeout_nanos = timeout.as_nanos();
let interval_nanos = interval.as_nanos().max(1);
let ticks = timeout_nanos.saturating_add(interval_nanos.saturating_sub(1)) / interval_nanos;
u64::try_from(ticks.max(1)).unwrap_or(u64::MAX)
}