use areev_cal::AreevFacade;
use areev_core::types::{Grain, Trigger, TriggerKind};
use areev_store::Areev;
use areev_trigger::{clock::FixedClock, Clock, EvalOptions, Evaluator};
use std::sync::{Arc, Barrier, Mutex};
const NS: &str = "ops";
const WF: &str = "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90";
const T0: i64 = 1_767_225_600_000;
fn rig(dir: &tempfile::TempDir, name: &str) -> (Arc<AreevFacade>, Arc<FixedClock>) {
let m = Areev::open(dir.path().join(name).to_str().unwrap()).unwrap();
(Arc::new(AreevFacade::new(m)), Arc::new(FixedClock::new(T0)))
}
fn evaluator(facade: &Arc<AreevFacade>, clock: &Arc<FixedClock>, node: &str) -> Evaluator {
let _ = node;
Evaluator {
facade: Arc::clone(facade),
clock: Arc::clone(clock) as Arc<dyn Clock>,
connector: None,
connector_code: None,
starter: None,
credentials: Default::default(),
ns: NS.into(),
principal: "user:test".into(),
}
}
#[test]
fn eight_concurrent_evaluators_produce_exactly_one_claim_per_due_trigger() {
for round in 0..12 {
let dir = tempfile::TempDir::new().unwrap();
let (facade, clock) = rig(&dir, &format!("m{round}.db"));
let t = Trigger::new(TriggerKind::Interval, WF)
.interval_secs(3600)
.created_at(T0)
.namespace(NS);
facade.with_store(|m| m.add(&t)).unwrap();
const N: usize = 8;
let barrier = Arc::new(Barrier::new(N));
let claims = Arc::new(Mutex::new(0usize));
let locked = Arc::new(Mutex::new(0usize));
std::thread::scope(|s| {
for node in 0..N {
let (facade, clock) = (Arc::clone(&facade), Arc::clone(&clock));
let (barrier, claims, locked) =
(Arc::clone(&barrier), Arc::clone(&claims), Arc::clone(&locked));
s.spawn(move || {
let ev = evaluator(&facade, &clock, &format!("node-{node}"));
let opts = EvalOptions { node: format!("node-{node}"), ..Default::default() };
barrier.wait();
let r = ev.run(&opts).unwrap();
*claims.lock().unwrap() += r.claimed;
*locked.lock().unwrap() += r.skipped_locked + r.skipped_not_due;
});
}
});
assert_eq!(
*claims.lock().unwrap(),
1,
"round {round}: exactly one evaluator may fire a due trigger"
);
assert_eq!(
*locked.lock().unwrap(),
N - 1,
"round {round}: every loser must report a skip, not an error"
);
}
}
#[test]
fn losing_the_race_is_never_an_error() {
let dir = tempfile::TempDir::new().unwrap();
let (facade, clock) = rig(&dir, "quiet.db");
let t = Trigger::new(TriggerKind::Interval, WF).interval_secs(3600).created_at(T0).namespace(NS);
facade.with_store(|m| m.add(&t)).unwrap();
let errors = Arc::new(Mutex::new(Vec::<String>::new()));
std::thread::scope(|s| {
for node in 0..6 {
let (facade, clock, errors) =
(Arc::clone(&facade), Arc::clone(&clock), Arc::clone(&errors));
s.spawn(move || {
let ev = evaluator(&facade, &clock, "n");
let opts = EvalOptions { node: format!("node-{node}"), ..Default::default() };
let r = ev.run(&opts).unwrap();
errors.lock().unwrap().extend(r.errors);
});
}
});
assert!(errors.lock().unwrap().is_empty(), "{:?}", errors.lock().unwrap());
}
#[test]
fn a_stalled_evaluator_cannot_write_back_behind_its_successor() {
let dir = tempfile::TempDir::new().unwrap();
let (facade, clock) = rig(&dir, "zombie.db");
let t = Trigger::new(TriggerKind::Interval, WF).interval_secs(60).created_at(T0).namespace(NS);
let hash = facade.with_store(|m| m.add(&t)).unwrap().to_hex();
let a = evaluator(&facade, &clock, "A");
let a_opts = EvalOptions {
node: "A".into(),
lease: std::time::Duration::from_millis(1),
..Default::default()
};
a.run(&a_opts).unwrap();
clock.advance(60_000);
let b = evaluator(&facade, &clock, "B");
let b_opts = EvalOptions { node: "B".into(), ..Default::default() };
let r = b.run(&b_opts).unwrap();
assert_eq!(r.claimed, 1, "an expired lease must not park a trigger forever");
let (state, _) = facade.with_store(|m| m.trigger_state(&hash)).unwrap().unwrap();
assert_eq!(state.fence, 2, "each claim advances the fence");
assert!(state.claimed_by.is_none(), "B released cleanly");
}
#[test]
fn clock_skew_does_not_change_whether_a_trigger_is_due() {
let dir = tempfile::TempDir::new().unwrap();
let (facade, _) = rig(&dir, "skew.db");
let t = Trigger::new(TriggerKind::Interval, WF).interval_secs(3600).created_at(T0).namespace(NS);
facade.with_store(|m| m.add(&t)).unwrap();
let on_time = Arc::new(FixedClock::new(T0));
let fast = Arc::new(FixedClock::new(T0 + 600_000));
let first = evaluator(&facade, &on_time, "A").run(&EvalOptions::default()).unwrap();
assert_eq!(first.claimed, 1);
let second = evaluator(&facade, &fast, "B").run(&EvalOptions::default()).unwrap();
assert_eq!(second.claimed, 0, "a skewed clock must not re-fire recent work");
assert_eq!(second.skipped_not_due, 1);
}
#[test]
fn a_second_handle_on_an_embedded_memory_is_refused_at_open() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("single.db");
let _first = Areev::open(path.to_str().unwrap()).unwrap();
let second = Areev::open(path.to_str().unwrap());
let err = second.err().expect("a second handle must be refused");
assert!(err.to_string().starts_with("STO-E002"), "{err}");
}