Lines
100 %
Functions
72.41 %
Branches
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use uuid::Uuid;
use wasmtime::Engine;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScriptLimits {
pub fuel: u64,
pub max_memory_pages: u64,
}
impl Default for ScriptLimits {
fn default() -> Self {
Self {
fuel: 1_000_000,
max_memory_pages: 64,
#[derive(Debug, Clone)]
pub struct ScriptCtx {
pub user_id: Uuid,
pub limits: ScriptLimits,
impl ScriptCtx {
#[must_use]
pub fn new(user_id: Uuid) -> Self {
user_id,
limits: ScriptLimits::default(),
pub fn with_limits(mut self, limits: ScriptLimits) -> Self {
self.limits = limits;
self
/// Cooperative interrupt signal shared between the SLYNK reader task (which
/// `interrupt()`s on `(:emacs-interrupt)`) and the eval task. Backed by a
/// monotonic generation counter rather than a boolean latch: an interrupt
/// increments the count, and a request asks "did the count move since I
/// started?" via [`InterruptHandle::generation`] + [`InterruptHandle::is_interrupted_since`].
///
/// The generation design is deliberate (a prior boolean latch was racy): there
/// is no shared flag to leave set, so an interrupt can never "stick" and poison
/// an unrelated later request. Each top-level operation snapshots the
/// generation at entry and compares against it; the next operation re-snapshots,
/// automatically discarding any interrupt that targeted the finished one — no
/// per-exit cleanup needed, on any terminal path.
#[derive(Debug, Clone, Default)]
pub struct InterruptHandle {
generation: Arc<AtomicU64>,
impl InterruptHandle {
pub(crate) fn new() -> Self {
Self::default()
/// Signals an interrupt by advancing the generation. Idempotent in effect:
/// any request whose baseline predates this call observes the interrupt.
pub fn interrupt(&self) {
self.generation.fetch_add(1, Ordering::SeqCst);
/// The current interrupt generation. The eval task keeps an "ack"
/// watermark of the highest generation already attributed to a finished
/// request; a new interrupt is one whose generation exceeds that watermark.
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::SeqCst)
/// Cooperative cancel handle for an in-flight eval. Wraps a clone of
/// the Session's [`wasmtime::Engine`]; the engine's epoch counter is
/// shared internally so a `.bump()` from any task takes effect on the
/// concurrent `func.call_async` running against the same engine.
/// Each Session sets `store.set_epoch_deadline(1)` before invoking
/// `nomi-eval`, so one bump trips the deadline and the eval traps
/// with `wasmtime::Trap::Interrupt` — surfaced upstream as
/// `EngineError::EpochInterrupt` and on the wire as
/// `(:error (:code interrupted ...))`.
#[derive(Clone, Debug)]
pub struct EpochBumper {
engine: Engine,
impl EpochBumper {
pub(crate) fn new(engine: Engine) -> Self {
Self { engine }
/// Increments the engine epoch. Returns immediately (no await).
/// Safe to call before, during, or after an eval; the next epoch
/// check inside any in-flight `call_async` traps.
pub fn bump(&self) {
self.engine.increment_epoch();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_limits_are_non_zero() {
let limits = ScriptLimits::default();
assert!(limits.fuel > 0);
assert!(limits.max_memory_pages > 0);
fn ctx_carries_user_id() {
let id = Uuid::new_v4();
let ctx = ScriptCtx::new(id);
assert_eq!(ctx.user_id, id);
fn with_limits_overrides_default() {
let limits = ScriptLimits {
fuel: 42,
max_memory_pages: 7,
};
let ctx = ScriptCtx::new(id).with_limits(limits);
assert_eq!(ctx.limits, limits);
fn interrupt_advances_generation_monotonically() {
let h = InterruptHandle::new();
let base = h.generation();
h.interrupt();
assert_eq!(h.generation(), base + 1);
assert_eq!(h.generation(), base + 2);
fn interrupt_handle_clones_share_generation() {
let a = InterruptHandle::new();
let base = a.generation();
let b = a.clone();
b.interrupt();
assert!(a.generation() > base);