use std::time::Duration;
use stateright::{Checker, Model, Property};
const A: u8 = 0; const N: u8 = 1; const P: u8 = 2; const T: u8 = 3; const CI: u8 = 4; const CS: u8 = 5; const E: u8 = 6; const AP: u8 = 7; const ND: u8 = 8; const NUM_LOCKS: usize = 9;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum Op {
Acquire(u8),
Release(u8),
}
use Op::{Acquire, Release};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct Task {
name: &'static str,
program: Vec<Op>,
pc: usize,
}
impl Task {
fn new(name: &'static str, program: Vec<Op>) -> Self {
Task {
name,
program,
pc: 0,
}
}
fn done(&self) -> bool {
self.pc >= self.program.len()
}
fn next_op(&self) -> Option<Op> {
self.program.get(self.pc).copied()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct State {
holder: [Option<usize>; NUM_LOCKS],
tasks: Vec<Task>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Step {
idx: usize,
}
fn poll_program() -> Vec<Op> {
vec![
Acquire(CI),
Release(CI),
Acquire(N),
Release(N),
Acquire(P),
Release(P),
Acquire(AP),
Release(AP),
Acquire(ND),
Release(ND),
Acquire(P),
Release(P),
Acquire(T),
Release(T),
Acquire(N),
Release(N),
Acquire(P),
Release(P),
Acquire(N),
Release(N),
Acquire(N),
Acquire(P),
Release(P),
Release(N),
Acquire(P),
Release(P),
Acquire(N),
Acquire(AP),
Release(AP),
Release(N),
Acquire(CI),
Release(CI),
Acquire(N),
Acquire(P),
Release(P),
Release(N),
Acquire(A),
Release(A),
Acquire(N),
Acquire(P),
Release(P),
Acquire(E),
Release(E),
Acquire(AP),
Release(AP),
Release(N),
Acquire(P),
Release(P),
Acquire(N),
Acquire(AP),
Release(AP),
Release(N),
]
}
fn seek_program() -> Vec<Op> {
vec![
Acquire(A),
Acquire(N),
Acquire(P),
Release(P),
Release(N),
Release(A),
]
}
fn at_log_end_program() -> Vec<Op> {
vec![
Acquire(A),
Acquire(N),
Acquire(E),
Release(E),
Release(N),
Release(A),
]
}
fn coordinator_program() -> Vec<Op> {
vec![
Acquire(A),
Release(A),
Acquire(CS),
Acquire(CI),
Release(CI),
Acquire(AP),
Release(AP),
Release(CS),
Acquire(T),
Release(T),
Acquire(ND),
Release(ND),
Acquire(A),
Acquire(CI),
Release(CI),
Release(A),
Acquire(N),
Acquire(P),
Release(P),
Release(N),
Acquire(A),
Release(A),
Acquire(T),
Release(T),
Acquire(N),
Acquire(P),
Release(P),
Release(N),
Acquire(A),
Acquire(CI),
Release(CI),
Release(A),
Acquire(CI),
Release(CI),
]
}
fn commit_program() -> Vec<Op> {
vec![
Acquire(CS),
Acquire(CI),
Acquire(N),
Release(N),
Release(CI),
Acquire(CI),
Release(CI),
Acquire(P),
Release(P),
Acquire(AP),
Release(AP),
Acquire(T),
Release(T),
Release(CS),
]
}
fn async_commit_program() -> Vec<Op> {
vec![
Acquire(CS),
Acquire(CI),
Release(CI),
Acquire(N),
Release(N),
Acquire(P),
Release(P),
Acquire(AP),
Release(AP),
Acquire(T),
Release(T),
Release(CS),
]
}
fn auto_commit_task_program() -> Vec<Op> {
vec![Acquire(CS), Acquire(ND), Release(ND), Release(CS)]
}
fn close_auto_commit_program() -> Vec<Op> {
vec![
Acquire(CS),
Acquire(CI),
Release(CI),
Acquire(N),
Release(N),
Acquire(P),
Release(P),
Acquire(CI),
Release(CI),
Acquire(P),
Release(P),
Acquire(AP),
Release(AP),
Release(CS),
]
}
#[derive(Clone, Debug)]
struct LockOrderModel {
programs: Vec<(&'static str, Vec<Op>)>,
}
impl Model for LockOrderModel {
type State = State;
type Action = Step;
fn init_states(&self) -> Vec<Self::State> {
let tasks = self
.programs
.iter()
.map(|(name, prog)| Task::new(name, prog.clone()))
.collect();
vec![State {
holder: [None; NUM_LOCKS],
tasks,
}]
}
fn actions(&self, s: &Self::State, acts: &mut Vec<Self::Action>) {
for (idx, task) in s.tasks.iter().enumerate() {
let Some(op) = task.next_op() else { continue };
let enabled = match op {
Op::Release(_) => true,
Op::Acquire(l) => match s.holder[l as usize] {
None => true,
Some(h) => h == idx,
},
};
if enabled {
acts.push(Step { idx });
}
}
}
fn next_state(&self, last: &Self::State, action: Self::Action) -> Option<Self::State> {
let mut s = last.clone();
let idx = action.idx;
let op = s.tasks[idx].next_op()?;
match op {
Op::Acquire(l) => {
match s.holder[l as usize] {
None => s.holder[l as usize] = Some(idx),
Some(h) if h == idx => {} Some(_) => return None, }
}
Op::Release(l) => {
if s.holder[l as usize] != Some(idx) {
return None;
}
s.holder[l as usize] = None;
}
}
s.tasks[idx].pc += 1;
Some(s)
}
fn properties(&self) -> Vec<Property<Self>> {
vec![
Property::always("no_deadlock", |_, s: &State| !is_deadlocked(s)),
Property::sometimes("all_tasks_complete", |_, s: &State| {
s.tasks.iter().all(Task::done)
}),
Property::sometimes("contention_observed", |_, s: &State| any_task_blocked(s)),
]
}
}
fn task_blocked(s: &State, idx: usize) -> bool {
match s.tasks[idx].next_op() {
Some(Op::Acquire(l)) => matches!(s.holder[l as usize], Some(h) if h != idx),
_ => false,
}
}
fn any_task_blocked(s: &State) -> bool {
(0..s.tasks.len()).any(|i| task_blocked(s, i))
}
fn is_deadlocked(s: &State) -> bool {
let unfinished: Vec<usize> = (0..s.tasks.len()).filter(|&i| !s.tasks[i].done()).collect();
!unfinished.is_empty() && unfinished.iter().all(|&i| task_blocked(s, i))
}
const MAX_STATES: usize = 5_000_000;
const MAX_DEPTH: usize = 256;
const CHECK_TIMEOUT: Duration = Duration::from_mins(1);
fn run_model(programs: Vec<(&'static str, Vec<Op>)>) -> stateright::CheckerBuilder<LockOrderModel> {
LockOrderModel { programs }
.checker()
.target_max_depth(MAX_DEPTH)
.target_state_count(MAX_STATES)
.timeout(CHECK_TIMEOUT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classic_consumer_lock_protocol_is_deadlock_free() {
let checker = run_model(vec![
("poll", poll_program()),
("at-log-end", at_log_end_program()),
("coordinator", coordinator_program()),
("commit-sync", commit_program()),
("commit-async", async_commit_program()),
])
.spawn_bfs()
.join();
eprintln!(
"[lock_order] unique={} generated={} depth={}",
checker.unique_state_count(),
checker.state_count(),
checker.max_depth(),
);
assert2::assert!(checker.state_count() < MAX_STATES);
assert2::assert!(checker.max_depth() < MAX_DEPTH);
checker.assert_properties();
}
#[test]
fn auto_commit_tasks_are_deadlock_free() {
let checker = run_model(vec![
("poll", poll_program()),
("coordinator", coordinator_program()),
("commit-async", async_commit_program()),
("auto-commit", auto_commit_task_program()),
("close-auto-commit", close_auto_commit_program()),
])
.spawn_bfs()
.join();
eprintln!(
"[lock_order/auto-commit] unique={} generated={} depth={}",
checker.unique_state_count(),
checker.state_count(),
checker.max_depth(),
);
assert2::assert!(checker.state_count() < MAX_STATES);
assert2::assert!(checker.max_depth() < MAX_DEPTH);
checker.assert_properties();
}
#[test]
fn seek_is_deadlock_free() {
let checker = run_model(vec![
("poll", poll_program()),
("seek", seek_program()),
("coordinator", coordinator_program()),
("commit-sync", commit_program()),
])
.spawn_bfs()
.join();
eprintln!(
"[lock_order/seek] unique={} generated={} depth={}",
checker.unique_state_count(),
checker.state_count(),
checker.max_depth(),
);
assert2::assert!(checker.state_count() < MAX_STATES);
assert2::assert!(checker.max_depth() < MAX_DEPTH);
checker.assert_properties();
}
#[test]
fn two_pollers_and_coordinator_are_deadlock_free() {
let checker = run_model(vec![
("poll-a", poll_program()),
("poll-b", poll_program()),
("coordinator", coordinator_program()),
])
.spawn_bfs()
.join();
eprintln!(
"[lock_order/2poll] unique={} generated={} depth={}",
checker.unique_state_count(),
checker.state_count(),
checker.max_depth(),
);
assert2::assert!(checker.state_count() < MAX_STATES);
assert2::assert!(checker.max_depth() < MAX_DEPTH);
checker.assert_properties();
}
#[test]
fn injected_inversion_is_detected_as_deadlock() {
let n_then_p = vec![Acquire(N), Acquire(P), Release(P), Release(N)];
let p_then_n = vec![Acquire(P), Acquire(N), Release(N), Release(P)];
let checker = run_model(vec![("np", n_then_p), ("pn", p_then_n)])
.spawn_bfs()
.join();
assert2::assert!(checker.discoveries().contains_key("no_deadlock"));
}
#[test]
fn deadlock_predicate_recognizes_a_mutual_wait_cycle() {
let mut s = State {
holder: [None; NUM_LOCKS],
tasks: vec![
Task::new("x", vec![Acquire(N), Acquire(P)]),
Task::new("y", vec![Acquire(P), Acquire(N)]),
],
};
s.holder[N as usize] = Some(0);
s.tasks[0].pc = 1; s.holder[P as usize] = Some(1);
s.tasks[1].pc = 1; assert2::assert!(is_deadlocked(&s));
let mut live = State {
holder: [None; NUM_LOCKS],
tasks: vec![
Task::new("x", vec![Acquire(N), Release(N), Acquire(P)]),
Task::new("y", vec![Acquire(P), Acquire(N)]),
],
};
live.holder[N as usize] = Some(0);
live.tasks[0].pc = 1; live.holder[P as usize] = Some(1);
live.tasks[1].pc = 1; assert2::assert!(!is_deadlocked(&live));
let all_done = State {
holder: [None; NUM_LOCKS],
tasks: vec![Task::new("x", vec![Acquire(N), Release(N)])],
};
let mut done = all_done;
done.tasks[0].pc = 2; assert2::assert!(!is_deadlocked(&done));
}
}