use stateright::{Checker, Model, Property};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum WatchPhase {
Expired,
Listed,
Fallback,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct St {
phase: WatchPhase,
bucket_k: bool,
k_in_list: bool,
fold_k: bool,
deletes_folded: bool,
relist_put_folded: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum Act {
ListLive,
RecreateK,
FoldSyntheticDeletes,
StartFallback,
DeliverRelistPut,
}
#[derive(Clone)]
struct ResyncOrder {
ack_barrier: bool,
}
impl Model for ResyncOrder {
type State = St;
type Action = Act;
fn init_states(&self) -> Vec<St> {
vec![St {
phase: WatchPhase::Expired,
bucket_k: false,
k_in_list: false,
fold_k: true,
deletes_folded: false,
relist_put_folded: false,
}]
}
fn actions(&self, s: &St, acts: &mut Vec<Act>) {
if s.phase == WatchPhase::Expired {
acts.push(Act::ListLive);
}
if !s.bucket_k {
acts.push(Act::RecreateK);
}
if s.phase != WatchPhase::Expired && !s.deletes_folded {
acts.push(Act::FoldSyntheticDeletes);
}
if s.phase == WatchPhase::Listed {
if !self.ack_barrier || s.deletes_folded {
acts.push(Act::StartFallback);
}
}
if s.phase == WatchPhase::Fallback && s.bucket_k && !s.relist_put_folded {
acts.push(Act::DeliverRelistPut);
}
}
fn next_state(&self, s: &St, a: Act) -> Option<St> {
let mut s = s.clone();
match a {
Act::ListLive => {
s.k_in_list = s.bucket_k;
s.phase = WatchPhase::Listed;
}
Act::RecreateK => s.bucket_k = true,
Act::FoldSyntheticDeletes => {
if !s.k_in_list {
s.fold_k = false;
}
s.deletes_folded = true;
}
Act::StartFallback => s.phase = WatchPhase::Fallback,
Act::DeliverRelistPut => {
s.fold_k = true;
s.relist_put_folded = true;
}
}
Some(s)
}
fn properties(&self) -> Vec<Property<Self>> {
let mut props: Vec<Property<Self>> = Vec::new();
if self.ack_barrier {
props.push(Property::<Self>::always(
"every maximal run ends with the fold equal to the bucket",
|m, s| {
let mut acts = Vec::new();
m.actions(s, &mut acts);
!acts.is_empty() || s.fold_k == s.bucket_k
},
));
props.push(Property::<Self>::sometimes(
"recreate-after-listing converges via delete-then-put ordering",
|_, s| !s.k_in_list && s.relist_put_folded && s.fold_k && s.bucket_k,
));
props.push(Property::<Self>::sometimes(
"recreate-before-listing converges via the listing itself",
|_, s| s.k_in_list && s.fold_k && s.bucket_k,
));
props.push(Property::<Self>::sometimes(
"never-recreated K is reconciled away",
|_, s| !s.fold_k && !s.bucket_k && s.deletes_folded,
));
} else {
props.push(Property::<Self>::sometimes(
"HAZARD reachable: synthetic delete erases a re-created key",
|m, s| {
let mut acts = Vec::new();
m.actions(s, &mut acts);
acts.is_empty() && s.bucket_k && !s.fold_k
},
));
}
props
}
}
fn check(ack_barrier: bool, label: &str) {
let model = ResyncOrder { ack_barrier };
let checker = model.checker().spawn_bfs().join();
println!(
"{label}: {} states, {} unique",
checker.state_count(),
checker.unique_state_count(),
);
checker.assert_properties();
}
#[test]
fn ack_barrier_makes_resync_ordering_converge() {
check(true, "resync order: ack barrier");
}
#[test]
fn without_ack_barrier_recreated_keys_are_lost() {
check(false, "resync order: no barrier");
}