use std::sync::Arc;
use std::time::Duration;
use vantage_core::{Result, error};
use vantage_rhai::rhai::{Dynamic, EvalAltResult, Map, Scope};
use vantage_rhai::{Host, Limits};
use super::transactions::{
FAILED_STATEMENT, PROBE_DEADLINE, Runner, TxFixture, TxTables, absent, ensure, present, run,
};
use crate::rhai::{
ATOMIC_ARM_TIMEOUT, ATOMIC_BLOCK_TIMEOUT, AtomicScope, AtomicVocab, DataVocab, ResolverCell,
Stop, Terminals, Writes,
};
use crate::{Vista, VistaResolver};
const NESTED: &str = "atomic can't be nested";
const BLOCK_LIMIT: Duration = Duration::from_secs(1);
const ARM_BUDGET: Duration = Duration::from_millis(200);
pub async fn check_atomic_rhai(fx: &impl TxFixture) {
let mut runner = Runner::new();
run(
&mut runner,
"atomic_block_commits",
PROBE_DEADLINE,
atomic_block_commits(fx),
)
.await;
run(
&mut runner,
"a_throw_rolls_back_and_runs_the_arm",
PROBE_DEADLINE,
a_throw_rolls_back_and_runs_the_arm(fx),
)
.await;
run(
&mut runner,
"a_throwing_arm_rethrows_the_first_error",
PROBE_DEADLINE,
a_throwing_arm_rethrows_the_first_error(fx),
)
.await;
run(
&mut runner,
"return_inside_atomic_commits",
PROBE_DEADLINE,
return_inside_atomic_commits(fx),
)
.await;
run(
&mut runner,
"a_handle_from_before_the_block_joins_it",
PROBE_DEADLINE,
a_handle_from_before_the_block_joins_it(fx),
)
.await;
run(
&mut runner,
"a_handle_outlives_its_block",
PROBE_DEADLINE,
a_handle_outlives_its_block(fx),
)
.await;
run(
&mut runner,
"a_failed_statement_fails_the_block",
PROBE_DEADLINE,
a_failed_statement_fails_the_block(fx),
)
.await;
run(
&mut runner,
"nested_atomic_is_refused",
PROBE_DEADLINE,
nested_atomic_is_refused(fx),
)
.await;
runner.finish();
}
async fn atomic_block_commits(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"atomic {
table("a").insert(#{ id: "x", n: 1 });
table("b").insert(#{ id: "y", a_id: "x", n: 1 });
}"#,
)
.await?;
if let Err(e) = ran.out {
return Err(error!(format!("the block throws `{e}`")));
}
present(&t.a, "x", "after the block").await?;
present(&t.b, "y", "after the block").await
}
async fn a_throw_rolls_back_and_runs_the_arm(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"let seen = ();
atomic {
table("a").insert(#{ id: "x", n: 1 });
throw "stop";
} rollback (e) {
seen = e;
}"#,
)
.await?;
threw(&ran, "stop")?;
ensure(
ran.seen.as_deref() == Some("stop"),
format!("the arm binds {:?}, not \"stop\"", ran.seen),
)?;
absent(&t.a, "x", "after the throw").await
}
async fn a_throwing_arm_rethrows_the_first_error(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"atomic {
table("a").insert(#{ id: "x", n: 1 });
throw "stop";
} rollback (e) {
throw "arm";
}"#,
)
.await?;
threw(&ran, "stop")?;
absent(&t.a, "x", "after the arm threw").await
}
async fn return_inside_atomic_commits(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"fn f() {
atomic {
table("a").insert(#{ id: "x", n: 1 });
return 7;
}
}
f()"#,
)
.await?;
match &ran.out {
Ok(v) if v.as_int() == Ok(7) => {}
Ok(v) => return Err(error!(format!("f() returns {v}, not 7"))),
Err(e) => return Err(error!(format!("f() throws `{e}`, not returns 7"))),
}
present(&t.a, "x", "after the return").await
}
async fn a_handle_from_before_the_block_joins_it(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"let t = table("a");
atomic {
t.insert(#{ id: "x", n: 1 });
throw "stop";
}"#,
)
.await?;
threw(&ran, "stop")?;
absent(&t.a, "x", "after the throw").await
}
async fn a_handle_outlives_its_block(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"let h;
atomic {
h = table("a");
h.insert(#{ id: "x", n: 1 });
}
h.get("x")"#,
)
.await?;
let row = ran
.out
.map_err(|e| error!(format!("the read after the block throws `{e}`")))?;
let n = row
.clone()
.try_cast::<Map>()
.and_then(|row| row.get("n").and_then(|n| n.as_int().ok()));
ensure(
n == Some(1),
format!("h.get(\"x\") after the block returns {row}, not a row with n = 1"),
)?;
present(&t.a, "x", "after the block").await
}
async fn a_failed_statement_fails_the_block(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"atomic {
table("a").insert(#{ id: "x", n: 1 });
try { table("a").insert(#{ id: "z", n: -1 }) } catch {}
}"#,
)
.await?;
let failed = matches!(&ran.out, Err(e) if e.contains(FAILED_STATEMENT));
ensure(
failed,
format!("the block ends {:?}, not `{FAILED_STATEMENT}`", ran.out),
)?;
absent(&t.a, "x", "after the failed block").await
}
async fn nested_atomic_is_refused(fx: &impl TxFixture) -> Result<()> {
let t = fx.fresh().await;
let ran = eval(
&t,
r#"atomic {
table("a").insert(#{ id: "x", n: 1 });
atomic { 1 }
}"#,
)
.await?;
threw(&ran, NESTED)?;
absent(&t.a, "x", "after the nested block").await
}
struct Ran {
out: std::result::Result<Dynamic, String>,
seen: Option<String>,
}
async fn eval(t: &TxTables, script: &'static str) -> Result<Ran> {
let (a, b) = (t.a.clone(), t.b.clone());
tokio::task::spawn_blocking(move || {
let host = host(a, b);
let mut scope = Scope::new();
let out = host
.engine()
.eval_with_scope::<Dynamic>(&mut scope, script)
.map_err(|e| message(&e));
Ran {
out,
seen: scope.get_value::<String>("seen"),
}
})
.await
.map_err(|e| error!(format!("the script's thread failed: {e}")))
}
fn host(a: Vista, b: Vista) -> Host {
let base: Arc<dyn VistaResolver> = Arc::new(move |name: &str| match name {
"a" => Ok(a.clone()),
"b" => Ok(b.clone()),
_ => Err(error!("no such table", name = name)),
});
let cell = ResolverCell::new(base);
let terminals = Terminals::ReadWrite {
limit: None,
writes: Writes::Allowed,
};
let scope = AtomicScope::new(cell.clone())
.with_limit(BLOCK_LIMIT)
.with_arm_budget(ARM_BUDGET);
let clock = scope.clock();
Host::builder(Limits::background())
.vocab(DataVocab::new(terminals).with_resolver(Arc::new(cell)))
.vocab(AtomicVocab::new(scope))
.vocab_fn(move |engine| {
engine.on_progress(move |_| {
clock.should_stop(false).map(|stop| {
Dynamic::from(match stop {
Stop::Cancelled => "cancelled",
Stop::BlockTimeout => ATOMIC_BLOCK_TIMEOUT,
Stop::ArmTimeout => ATOMIC_ARM_TIMEOUT,
})
})
});
})
.build()
}
fn message(err: &EvalAltResult) -> String {
match err {
EvalAltResult::ErrorRuntime(value, _) => value.to_string(),
other => other.to_string(),
}
}
fn threw(ran: &Ran, wanted: &str) -> Result<()> {
match &ran.out {
Err(e) if e == wanted => Ok(()),
Err(e) => Err(error!(format!("the script throws `{e}`, not `{wanted}`"))),
Ok(v) => Err(error!(format!("the script returns {v}, not `{wanted}`"))),
}
}