use std::fmt;
pub use melin_transport_core::ack_policy::{
Blocker, Clause, CursorView, EvalStatus, Level, MAX_CLUSTER_SIZE, Policy, PolicyError,
};
pub const ACK_POLICY_UNKNOWN: u8 = u8::MAX;
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum AckPolicy {
Disk,
Ram,
#[value(name = "disk+ram")]
DiskAndRam,
TwoDisks,
}
impl AckPolicy {
pub fn to_policy(self) -> Policy {
let clauses = match self {
AckPolicy::Disk => vec![Clause {
count: 1,
level: Level::Persisted,
}],
AckPolicy::Ram => vec![Clause {
count: 2,
level: Level::InMemory,
}],
AckPolicy::DiskAndRam => vec![
Clause {
count: 1,
level: Level::Persisted,
},
Clause {
count: 2,
level: Level::InMemory,
},
],
AckPolicy::TwoDisks => vec![Clause {
count: 2,
level: Level::Persisted,
}],
};
Policy::new(clauses).expect("AckPolicy::to_policy: hand-constructed clauses must validate")
}
pub fn as_str(self) -> &'static str {
match self {
AckPolicy::Disk => "disk",
AckPolicy::Ram => "ram",
AckPolicy::DiskAndRam => "disk+ram",
AckPolicy::TwoDisks => "two-disks",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"disk" => Some(AckPolicy::Disk),
"ram" => Some(AckPolicy::Ram),
"disk+ram" => Some(AckPolicy::DiskAndRam),
"two-disks" => Some(AckPolicy::TwoDisks),
_ => None,
}
}
pub fn parse_legacy(s: &str) -> Option<Self> {
match s {
"local" => Some(AckPolicy::Disk),
"replicated" => Some(AckPolicy::Ram),
"hybrid" => Some(AckPolicy::DiskAndRam),
"durably-replicated" => Some(AckPolicy::TwoDisks),
_ => Self::parse(s),
}
}
pub fn as_u8(self) -> u8 {
match self {
AckPolicy::Disk => 0,
AckPolicy::DiskAndRam => 1,
AckPolicy::TwoDisks => 2,
AckPolicy::Ram => 3,
}
}
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0 => Some(AckPolicy::Disk),
1 => Some(AckPolicy::DiskAndRam),
2 => Some(AckPolicy::TwoDisks),
3 => Some(AckPolicy::Ram),
_ => None,
}
}
}
impl fmt::Display for AckPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::ValueEnum;
const ALL: [AckPolicy; 4] = [
AckPolicy::Disk,
AckPolicy::DiskAndRam,
AckPolicy::TwoDisks,
AckPolicy::Ram,
];
#[test]
fn u8_round_trip() {
for p in ALL {
assert_eq!(AckPolicy::from_u8(p.as_u8()), Some(p));
}
for b in [4, 5, 255] {
assert_eq!(AckPolicy::from_u8(b), None);
}
}
#[test]
fn wire_bytes_are_pinned() {
assert_eq!(AckPolicy::Disk.as_u8(), 0);
assert_eq!(AckPolicy::DiskAndRam.as_u8(), 1);
assert_eq!(AckPolicy::TwoDisks.as_u8(), 2);
assert_eq!(AckPolicy::Ram.as_u8(), 3);
}
#[test]
fn parse_matches_as_str() {
for p in ALL {
assert_eq!(AckPolicy::parse(p.as_str()), Some(p));
}
for bad in [
"",
"DISK",
"dis",
"disk-ram",
"disk-and-ram",
"two-disk",
"disks",
"local",
"hybrid",
"replicated",
"durably-replicated",
] {
assert_eq!(AckPolicy::parse(bad), None, "{bad:?} should not parse");
}
}
#[test]
fn parse_legacy_maps_old_names_and_accepts_new_ones() {
assert_eq!(AckPolicy::parse_legacy("local"), Some(AckPolicy::Disk));
assert_eq!(AckPolicy::parse_legacy("replicated"), Some(AckPolicy::Ram));
assert_eq!(
AckPolicy::parse_legacy("hybrid"),
Some(AckPolicy::DiskAndRam)
);
assert_eq!(
AckPolicy::parse_legacy("durably-replicated"),
Some(AckPolicy::TwoDisks)
);
for p in ALL {
assert_eq!(AckPolicy::parse_legacy(p.as_str()), Some(p));
}
assert_eq!(AckPolicy::parse_legacy("fast"), None);
}
#[test]
fn clap_spelling_matches_as_str() {
for p in ALL {
let clap_name = p.to_possible_value().expect("every variant is a CLI value");
assert_eq!(clap_name.get_name(), p.as_str());
}
}
#[test]
fn disk_builds_persisted_ge_1() {
let p = AckPolicy::Disk.to_policy();
assert_eq!(p.clauses().len(), 1);
let c = p.clauses()[0];
assert_eq!(c.level, Level::Persisted);
assert_eq!(c.count, 1);
}
#[test]
fn disk_and_ram_builds_persisted_ge_1_and_in_memory_ge_2() {
let p = AckPolicy::DiskAndRam.to_policy();
assert_eq!(p.clauses().len(), 2);
let persisted = p
.clauses()
.iter()
.find(|c| c.level == Level::Persisted)
.expect("persisted clause");
assert_eq!(persisted.count, 1);
let in_mem = p
.clauses()
.iter()
.find(|c| c.level == Level::InMemory)
.expect("in_memory clause");
assert_eq!(in_mem.count, 2);
}
#[test]
fn ram_builds_in_memory_ge_2() {
let p = AckPolicy::Ram.to_policy();
assert_eq!(p.clauses().len(), 1);
let c = p.clauses()[0];
assert_eq!(c.level, Level::InMemory);
assert_eq!(c.count, 2);
}
#[test]
fn ram_fails_closed_on_single_node() {
let p = AckPolicy::Ram.to_policy();
let nodes = [[100u64, 100u64]];
let v = CursorView::new(&nodes);
assert_eq!(p.evaluate(&v), 0, "ram stalls on single-node view");
}
#[test]
fn ram_ignores_every_persisted_cursor() {
let p = AckPolicy::Ram.to_policy();
let nodes = [[100u64, 10u64], [80u64, 0u64]];
let v = CursorView::new(&nodes);
assert_eq!(p.evaluate(&v), 80);
}
#[test]
fn two_disks_builds_persisted_ge_2() {
let p = AckPolicy::TwoDisks.to_policy();
assert_eq!(p.clauses().len(), 1);
let c = p.clauses()[0];
assert_eq!(c.level, Level::Persisted);
assert_eq!(c.count, 2);
}
#[test]
fn no_policy_gates_on_the_primary_in_memory_sentinel() {
for p in ALL {
for c in p.to_policy().clauses() {
assert!(
c.level == Level::Persisted || c.count >= 2,
"policy {p}: clause `{c}` would be satisfied by the primary's \
in-memory sentinel alone"
);
}
}
}
#[test]
fn disk_and_ram_fails_closed_on_single_node() {
let p = AckPolicy::DiskAndRam.to_policy();
let nodes = [[100u64, 100u64]];
let v = CursorView::new(&nodes);
assert_eq!(p.evaluate(&v), 0, "disk+ram stalls on single-node view");
}
#[test]
fn disk_and_ram_advances_with_two_nodes_acking_in_memory() {
let p = AckPolicy::DiskAndRam.to_policy();
let nodes = [[100u64, 100u64], [80u64, 0u64]];
let v = CursorView::new(&nodes);
assert_eq!(p.evaluate(&v), 80);
}
#[test]
fn disk_is_satisfied_by_whichever_node_fsyncs_first() {
let p = AckPolicy::Disk.to_policy();
let nodes = [[100u64, 10u64], [90u64, 90u64]];
let v = CursorView::new(&nodes);
assert_eq!(p.evaluate(&v), 90);
}
}