use std::cell::RefCell;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use vantage_rhai::rhai::{Dynamic, Engine, EvalAltResult, EvalContext, Expression, Position};
use vantage_rhai::{Vocab, register_atomic_syntax};
use super::bridge::block_on;
use super::cell::ResolverCell;
use super::read::{RhaiResult, rhai_err};
use crate::transaction::{Committed, Transaction};
pub const ATOMIC_BLOCK_TIMEOUT: &str = "atomic block ran out of time";
pub const ATOMIC_ARM_TIMEOUT: &str = "atomic rollback arm ran out of time";
const NESTED: &str = "atomic can't be nested";
const LEFT_BY_LOOP: &str = "break and continue can't leave an atomic block";
pub type OnCommit = Arc<dyn Fn(&Committed) + Send + Sync>;
type Arm<'a> = &'a mut dyn FnMut(Box<EvalAltResult>) -> RhaiResult<()>;
#[derive(Clone)]
pub struct AtomicScope(Arc<ScopeInner>);
#[derive(Clone)]
struct ScopeInner {
cell: ResolverCell,
limit: Duration,
arm_budget: Duration,
on_commit: Option<OnCommit>,
clock: AtomicClock,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Stop {
Cancelled,
BlockTimeout,
ArmTimeout,
}
#[derive(Clone, Default)]
pub struct AtomicClock(Arc<ClockInner>);
#[derive(Default)]
struct ClockInner {
phase: Mutex<Phase>,
}
#[derive(Clone, Copy, Default)]
enum Phase {
#[default]
Idle,
Block {
deadline: Instant,
},
Arm {
deadline: Instant,
},
}
struct Leave<'a>(&'a AtomicClock);
impl AtomicScope {
pub fn new(cell: ResolverCell) -> Self {
Self(Arc::new(ScopeInner {
cell,
limit: Duration::from_secs(30),
arm_budget: Duration::from_secs(10),
on_commit: None,
clock: AtomicClock::default(),
}))
}
pub fn with_limit(self, limit: Duration) -> Self {
self.map(|inner| inner.limit = limit)
}
pub fn with_arm_budget(self, budget: Duration) -> Self {
self.map(|inner| inner.arm_budget = budget)
}
pub fn on_commit(self, hook: OnCommit) -> Self {
self.map(|inner| inner.on_commit = Some(hook))
}
fn map(self, change: impl FnOnce(&mut ScopeInner)) -> Self {
let mut inner = Arc::unwrap_or_clone(self.0);
change(&mut inner);
Self(Arc::new(inner))
}
pub fn clock(&self) -> AtomicClock {
self.0.clock.clone()
}
pub fn is_active(&self) -> bool {
matches!(*self.0.clock.phase(), Phase::Block { .. })
}
pub fn run<T>(&self, body: impl FnOnce() -> RhaiResult<T>, arm: Option<Arm>) -> RhaiResult<T> {
let ScopeInner {
cell,
limit,
arm_budget,
clock,
..
} = &*self.0;
let deadline = Instant::now() + *limit;
let tx = Transaction::new();
let outcome = {
let _block = clock.enter(Phase::Block { deadline })?;
let _pointed = cell.point(tx.resolver(cell.base()));
body()
};
let overran = Instant::now() >= deadline;
let ended = match &outcome {
Ok(_) => true,
Err(e) => matches!(**e, EvalAltResult::Return(..) | EvalAltResult::Exit(..)),
};
let err = match outcome {
_ if overran && ended => Box::new(EvalAltResult::ErrorTerminated(
ATOMIC_BLOCK_TIMEOUT.into(),
Position::NONE,
)),
Ok(value) => match self.commit(&tx) {
Ok(()) => return Ok(value),
Err(err) => err,
},
Err(ret) if matches!(*ret, EvalAltResult::Return(..) | EvalAltResult::Exit(..)) => {
match self.commit(&tx) {
Ok(()) => return Err(ret),
Err(err) => err,
}
}
Err(err) => err,
};
if let Err(failed) = block_on(tx.rollback()) {
tracing::warn!("atomic rollback failed: {failed}");
}
let replaced = if matches!(*err, EvalAltResult::LoopBreak(..)) {
Some(LEFT_BY_LOOP)
} else if overran && matches!(err.unwrap_inner(), EvalAltResult::ErrorTerminated(..)) {
Some(ATOMIC_BLOCK_TIMEOUT)
} else {
None
};
let err = match replaced {
Some(text) => Box::new(EvalAltResult::ErrorRuntime(text.into(), err.position())),
None => err,
};
if let Some(arm) = arm
&& let Ok(_arm) = clock.enter(Phase::Arm {
deadline: Instant::now() + *arm_budget,
})
&& let Err(failed) = arm(for_arm(&err))
{
tracing::warn!("atomic rollback arm failed: {failed}");
}
Err(err)
}
fn commit(&self, tx: &Transaction) -> RhaiResult<()> {
let committed = block_on(tx.commit()).and_then(|r| r).map_err(rhai_err)?;
if let Some(hook) = &self.0.on_commit {
hook(&committed);
}
Ok(())
}
}
impl AtomicClock {
pub fn should_stop(&self, cancelled: bool) -> Option<Stop> {
let now = Instant::now();
match *self.phase() {
Phase::Arm { deadline } => (now >= deadline).then_some(Stop::ArmTimeout),
Phase::Block { deadline } if now >= deadline => Some(Stop::BlockTimeout),
_ => cancelled.then_some(Stop::Cancelled),
}
}
pub fn remaining(&self) -> Option<Duration> {
match *self.phase() {
Phase::Block { deadline } | Phase::Arm { deadline } => {
Some(deadline.saturating_duration_since(Instant::now()))
}
Phase::Idle => None,
}
}
fn phase(&self) -> MutexGuard<'_, Phase> {
self.0.phase.lock().unwrap_or_else(PoisonError::into_inner)
}
fn enter(&self, phase: Phase) -> RhaiResult<Leave<'_>> {
let mut current = self.phase();
if !matches!(*current, Phase::Idle) {
return Err(NESTED.into());
}
*current = phase;
Ok(Leave(self))
}
}
impl Drop for Leave<'_> {
fn drop(&mut self) {
*self.0.phase() = Phase::Idle;
}
}
fn for_arm(err: &EvalAltResult) -> Box<EvalAltResult> {
let value = match err.unwrap_inner() {
EvalAltResult::ErrorRuntime(value, _) | EvalAltResult::ErrorTerminated(value, _) => {
value.clone()
}
other => other.to_string().into(),
};
Box::new(EvalAltResult::ErrorRuntime(value, err.position()))
}
pub struct AtomicVocab(AtomicScope);
impl AtomicVocab {
pub fn new(scope: AtomicScope) -> Self {
Self(scope)
}
}
impl Vocab for AtomicVocab {
fn register(&self, engine: &mut Engine) {
let scope = self.0.clone();
register_atomic_syntax(engine, move |context, inputs| {
eval_atomic(&scope, context, inputs)
});
}
}
fn eval_atomic(
scope: &AtomicScope,
context: &mut EvalContext,
inputs: &[Expression],
) -> RhaiResult<Dynamic> {
let context = RefCell::new(context);
let body = || context.borrow_mut().eval_expression_tree(&inputs[0]);
let [_, name, arm] = inputs else {
return scope.run(body, None);
};
let name = name.get_string_value().unwrap_or_default().to_string();
let mut arm = |err: Box<EvalAltResult>| -> RhaiResult<()> {
let mut context = context.borrow_mut();
let bound = match *err {
EvalAltResult::ErrorRuntime(value, _) => value,
other => other.to_string().into(),
};
let depth = context.scope().len();
context.scope_mut().push_dynamic(name.as_str(), bound);
let out = context.eval_expression_tree(arm);
context.scope_mut().rewind(depth);
out.map(|_| ())
};
scope.run(body, Some(&mut arm))
}