#[derive(Clone, Copy, Debug)]
pub(crate) struct SimEvent {
pub(crate) seq: u64,
pub(crate) prev: u64,
pub(crate) hash: u64,
pub(crate) durable: bool,
}
#[derive(Default)]
pub(crate) struct ModelState {
pub(crate) log: Vec<SimEvent>,
pub(crate) visible_frontier: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Violation {
ChainBreak {
seq: u64,
expected_prev: u64,
found_prev: u64,
},
FrontierRegression {
previous: u64,
observed: u64,
},
DurableLoss {
seq: u64,
},
}
impl ModelState {
pub(crate) fn append(&mut self, event: SimEvent) {
if event.durable {
self.visible_frontier = self.visible_frontier.max(event.seq);
}
self.log.push(event);
}
pub(crate) fn chain_head(&self) -> u64 {
self.log
.iter()
.rev()
.find(|e| e.durable)
.map_or(0, |e| e.hash)
}
pub(crate) fn check(&self, prev_frontier: u64) -> Result<(), Violation> {
if self.visible_frontier < prev_frontier {
return Err(Violation::FrontierRegression {
previous: prev_frontier,
observed: self.visible_frontier,
});
}
let mut head = 0u64;
for event in self.log.iter().filter(|e| e.durable) {
if event.prev != head {
return Err(Violation::ChainBreak {
seq: event.seq,
expected_prev: head,
found_prev: event.prev,
});
}
head = event.hash;
}
Ok(())
}
pub(crate) fn check_no_loss(&self, pre_crash: &[SimEvent]) -> Result<(), Violation> {
for event in pre_crash.iter().filter(|e| e.durable) {
let survived = self.log.iter().any(|e| e.seq == event.seq);
if !survived {
return Err(Violation::DurableLoss { seq: event.seq });
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(seq: u64, prev: u64, hash: u64, durable: bool) -> SimEvent {
SimEvent {
seq,
prev,
hash,
durable,
}
}
#[test]
fn continuous_chain_passes() {
let mut m = ModelState::default();
m.append(ev(0, 0, 11, true));
m.append(ev(1, 11, 22, true));
assert!(m.check(0).is_ok(), "PROPERTY: an unbroken chain passes");
assert_eq!(m.visible_frontier, 1);
}
#[test]
fn chain_break_is_detected() {
let mut m = ModelState::default();
m.append(ev(0, 0, 11, true));
m.append(ev(1, 999, 22, true)); assert_eq!(
m.check(0),
Err(Violation::ChainBreak {
seq: 1,
expected_prev: 11,
found_prev: 999
}),
"PROPERTY: a broken prev-link is caught with full context"
);
}
#[test]
fn frontier_regression_is_detected() {
let m = ModelState::default();
assert_eq!(
m.check(5),
Err(Violation::FrontierRegression {
previous: 5,
observed: 0
}),
"PROPERTY: a visible-frontier regression is caught"
);
}
#[test]
fn durable_loss_after_crash_is_detected() {
let pre = vec![ev(0, 0, 11, true), ev(1, 11, 22, true)];
let mut recovered = ModelState::default();
recovered.append(ev(0, 0, 11, true)); assert_eq!(
recovered.check_no_loss(&pre),
Err(Violation::DurableLoss { seq: 1 }),
"PROPERTY: a durably-acked event missing after recovery is a no-loss violation"
);
}
}