use std::collections::HashMap;
use std::fmt;
use super::identity::Digest256;
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct Incarnation(u64);
impl Incarnation {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn get(self) -> u64 {
self.0
}
}
impl fmt::Display for Incarnation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "inc#{}", self.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct PersistenceFault;
impl fmt::Display for PersistenceFault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("incarnation counter persistence fault")
}
}
impl std::error::Error for PersistenceFault {}
pub trait IncarnationPersistence {
fn load(&mut self) -> Result<Option<u64>, PersistenceFault>;
fn store(&mut self, value: u64) -> Result<(), PersistenceFault>;
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IncarnationError {
Exhausted,
Persistence(PersistenceFault),
}
impl fmt::Display for IncarnationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exhausted => f.write_str("incarnation counter exhausted"),
Self::Persistence(fault) => fault.fmt(f),
}
}
}
impl std::error::Error for IncarnationError {}
pub fn next_incarnation<P: IncarnationPersistence>(
persistence: &mut P,
) -> Result<Incarnation, IncarnationError> {
let prev = persistence
.load()
.map_err(IncarnationError::Persistence)?
.unwrap_or(0);
let next = prev.checked_add(1).ok_or(IncarnationError::Exhausted)?;
persistence
.store(next)
.map_err(IncarnationError::Persistence)?;
Ok(Incarnation::new(next))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Admission {
Admit,
StaleSeq,
StaleIncarnation,
Equivocation,
PoisonedIncarnation,
}
impl Admission {
pub const fn is_admitted(self) -> bool {
matches!(self, Self::Admit)
}
}
#[derive(Clone, Copy)]
struct GateEntry {
incarnation: Incarnation,
last_seq: u64,
last_fingerprint: Digest256,
poisoned_at: Option<Incarnation>,
touch: u64,
}
#[derive(Default)]
pub struct IncarnationSeqGate {
entries: HashMap<(u64, Digest256), GateEntry>,
tick: u64,
}
impl IncarnationSeqGate {
const MAX_ENTRIES: usize = 8192;
const LOW_WATER: usize = 6144;
pub fn new() -> Self {
Self::default()
}
pub fn admit(
&mut self,
origin: u64,
digest: Digest256,
incarnation: Incarnation,
seq: u64,
fingerprint: Digest256,
) -> Admission {
let verdict = self.sight(origin, digest, incarnation, seq, fingerprint);
self.evict_if_over_capacity();
verdict
}
fn sight(
&mut self,
origin: u64,
digest: Digest256,
incarnation: Incarnation,
seq: u64,
fingerprint: Digest256,
) -> Admission {
let touch = self.tick;
self.tick += 1;
let entry = match self.entries.get_mut(&(origin, digest)) {
None => {
self.entries.insert(
(origin, digest),
GateEntry {
incarnation,
last_seq: seq,
last_fingerprint: fingerprint,
poisoned_at: None,
touch,
},
);
return Admission::Admit;
}
Some(entry) => entry,
};
entry.touch = touch;
if let Some(poisoned) = entry.poisoned_at {
if incarnation <= poisoned {
return Admission::PoisonedIncarnation;
}
}
match incarnation.cmp(&entry.incarnation) {
std::cmp::Ordering::Greater => {
entry.incarnation = incarnation;
entry.last_seq = seq;
entry.last_fingerprint = fingerprint;
Admission::Admit
}
std::cmp::Ordering::Less => Admission::StaleIncarnation,
std::cmp::Ordering::Equal => {
if seq > entry.last_seq {
entry.last_seq = seq;
entry.last_fingerprint = fingerprint;
Admission::Admit
} else if seq == entry.last_seq && fingerprint != entry.last_fingerprint {
entry.poisoned_at = Some(incarnation);
Admission::Equivocation
} else {
Admission::StaleSeq
}
}
}
}
fn evict_if_over_capacity(&mut self) {
if self.entries.len() <= Self::MAX_ENTRIES {
return;
}
let excess = self.entries.len() - Self::LOW_WATER;
let mut victims: Vec<(bool, u64, (u64, Digest256))> = self
.entries
.iter()
.map(|(key, entry)| (entry.poisoned_at.is_some(), entry.touch, *key))
.collect();
victims.select_nth_unstable_by_key(excess - 1, |&(poisoned, touch, _)| (poisoned, touch));
for &(_, _, key) in &victims[..excess] {
self.entries.remove(&key);
}
}
pub fn poisoned(&self, origin: u64, digest: Digest256) -> Option<Incarnation> {
self.entries
.get(&(origin, digest))
.and_then(|entry| entry.poisoned_at)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct FakeDisk {
value: Option<u64>,
fail_load: bool,
fail_store: bool,
stores: Vec<u64>,
}
impl IncarnationPersistence for FakeDisk {
fn load(&mut self) -> Result<Option<u64>, PersistenceFault> {
if self.fail_load {
return Err(PersistenceFault);
}
Ok(self.value)
}
fn store(&mut self, value: u64) -> Result<(), PersistenceFault> {
if self.fail_store {
return Err(PersistenceFault);
}
self.value = Some(value);
self.stores.push(value);
Ok(())
}
}
fn fp(byte: u8) -> Digest256 {
Digest256::from_bytes([byte; 32])
}
const DIGEST: [u8; 32] = [7u8; 32];
fn digest() -> Digest256 {
Digest256::from_bytes(DIGEST)
}
#[test]
fn boots_are_monotonic_and_persisted_before_use() {
let mut disk = FakeDisk::default();
for expected in 1..=3u64 {
let inc = next_incarnation(&mut disk).unwrap();
assert_eq!(inc, Incarnation::new(expected));
assert_eq!(disk.value, Some(expected));
}
assert_eq!(disk.stores, vec![1, 2, 3]);
}
#[test]
fn store_failure_blocks_participation_without_burning_the_value() {
let mut disk = FakeDisk {
value: Some(4),
fail_store: true,
..FakeDisk::default()
};
assert_eq!(
next_incarnation(&mut disk),
Err(IncarnationError::Persistence(PersistenceFault)),
);
assert_eq!(disk.value, Some(4), "failed store must not advance state");
disk.fail_store = false;
assert_eq!(next_incarnation(&mut disk), Ok(Incarnation::new(5)));
}
#[test]
fn unreadable_counter_fails_closed() {
let mut disk = FakeDisk {
value: Some(9),
fail_load: true,
..FakeDisk::default()
};
assert_eq!(
next_incarnation(&mut disk),
Err(IncarnationError::Persistence(PersistenceFault)),
);
}
#[test]
fn fresh_install_starts_at_one() {
let mut disk = FakeDisk::default();
assert_eq!(next_incarnation(&mut disk), Ok(Incarnation::new(1)));
}
#[test]
fn counter_exhaustion_refuses_participation() {
let mut disk = FakeDisk {
value: Some(u64::MAX),
..FakeDisk::default()
};
assert_eq!(
next_incarnation(&mut disk),
Err(IncarnationError::Exhausted)
);
assert_eq!(disk.value, Some(u64::MAX), "exhaustion must not wrap");
}
#[test]
fn strictly_newer_seq_admits_and_stale_drops() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(3);
assert!(gate.admit(1, digest(), inc, 5, fp(1)).is_admitted());
assert!(gate.admit(1, digest(), inc, 6, fp(2)).is_admitted());
assert_eq!(gate.admit(1, digest(), inc, 6, fp(2)), Admission::StaleSeq);
assert_eq!(gate.admit(1, digest(), inc, 4, fp(3)), Admission::StaleSeq);
}
#[test]
fn new_incarnation_supersedes_old_sequence_space() {
let mut gate = IncarnationSeqGate::new();
assert!(gate
.admit(1, digest(), Incarnation::new(7), 100, fp(1))
.is_admitted());
assert!(gate
.admit(1, digest(), Incarnation::new(8), 1, fp(2))
.is_admitted());
assert_eq!(
gate.admit(1, digest(), Incarnation::new(7), 101, fp(3)),
Admission::StaleIncarnation,
);
}
#[test]
fn rollback_is_contained_by_the_observer_gate() {
let mut gate = IncarnationSeqGate::new();
assert!(gate
.admit(1, digest(), Incarnation::new(5), 9, fp(1))
.is_admitted());
for rolled_back in 3..=5u64 {
assert_eq!(
gate.admit(1, digest(), Incarnation::new(rolled_back), 1, fp(2)),
if rolled_back < 5 {
Admission::StaleIncarnation
} else {
Admission::StaleSeq
},
);
}
assert!(gate
.admit(1, digest(), Incarnation::new(6), 1, fp(2))
.is_admitted());
}
#[test]
fn cloned_identity_poisons_the_incarnation_instead_of_flapping() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(7);
assert!(gate.admit(1, digest(), inc, 1, fp(0xA1)).is_admitted());
assert!(gate.admit(1, digest(), inc, 2, fp(0xA2)).is_admitted());
assert_eq!(
gate.admit(1, digest(), inc, 2, fp(0xB2)),
Admission::Equivocation,
);
assert_eq!(gate.poisoned(1, digest()), Some(inc));
assert_eq!(
gate.admit(1, digest(), inc, 3, fp(0xA3)),
Admission::PoisonedIncarnation,
);
assert_eq!(
gate.admit(1, digest(), inc, 3, fp(0xB3)),
Admission::PoisonedIncarnation,
);
assert!(gate
.admit(1, digest(), Incarnation::new(8), 1, fp(0xC1))
.is_admitted());
assert_eq!(gate.poisoned(1, digest()), Some(inc));
}
#[test]
fn identical_duplicate_is_a_relay_dup_not_equivocation() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(2);
assert!(gate.admit(1, digest(), inc, 4, fp(9)).is_admitted());
assert_eq!(gate.admit(1, digest(), inc, 4, fp(9)), Admission::StaleSeq);
assert_eq!(gate.poisoned(1, digest()), None);
}
#[test]
fn streams_are_independent_per_origin_and_digest() {
let mut gate = IncarnationSeqGate::new();
let other_digest = Digest256::from_bytes([8u8; 32]);
assert!(gate
.admit(1, digest(), Incarnation::new(5), 10, fp(1))
.is_admitted());
assert!(gate
.admit(2, digest(), Incarnation::new(1), 1, fp(2))
.is_admitted());
assert!(gate
.admit(1, other_digest, Incarnation::new(1), 1, fp(3))
.is_admitted());
assert_eq!(gate.len(), 3);
}
#[test]
fn eviction_drops_oldest_touched_idle_streams_to_low_water() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(1);
for origin in 0..(IncarnationSeqGate::MAX_ENTRIES as u64) {
assert!(gate.admit(origin, digest(), inc, 1, fp(1)).is_admitted());
}
assert_eq!(gate.len(), IncarnationSeqGate::MAX_ENTRIES);
let tipping = IncarnationSeqGate::MAX_ENTRIES as u64;
assert!(gate.admit(tipping, digest(), inc, 1, fp(1)).is_admitted());
assert_eq!(gate.len(), IncarnationSeqGate::LOW_WATER);
assert_eq!(
gate.admit(tipping, digest(), inc, 1, fp(1)),
Admission::StaleSeq,
);
assert_eq!(
gate.admit(tipping - 1, digest(), inc, 1, fp(1)),
Admission::StaleSeq,
);
assert!(gate.admit(0, digest(), inc, 1, fp(1)).is_admitted());
}
#[test]
fn active_stream_ordering_survives_one_shot_churn() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(3);
let hot = u64::MAX;
assert!(gate.admit(hot, digest(), inc, 100, fp(1)).is_admitted());
let mut hot_seq = 100;
for origin in 0..(3 * IncarnationSeqGate::MAX_ENTRIES as u64) {
assert!(gate.admit(origin, digest(), inc, 1, fp(2)).is_admitted());
if origin % 1024 == 0 {
hot_seq += 1;
assert!(gate.admit(hot, digest(), inc, hot_seq, fp(3)).is_admitted());
}
}
assert_eq!(
gate.admit(hot, digest(), inc, 50, fp(4)),
Admission::StaleSeq,
);
assert!(gate
.admit(hot, digest(), inc, hot_seq + 1, fp(5))
.is_admitted());
}
#[test]
fn poisoned_stream_survives_eviction_pressure_and_still_refuses() {
let mut gate = IncarnationSeqGate::new();
let inc = Incarnation::new(7);
let cloned = u64::MAX;
assert!(gate.admit(cloned, digest(), inc, 1, fp(0xA1)).is_admitted());
assert_eq!(
gate.admit(cloned, digest(), inc, 1, fp(0xB1)),
Admission::Equivocation,
);
assert_eq!(gate.poisoned(cloned, digest()), Some(inc));
for origin in 0..(3 * IncarnationSeqGate::MAX_ENTRIES as u64) {
assert!(gate
.admit(origin, digest(), Incarnation::new(1), 1, fp(2))
.is_admitted());
}
assert_eq!(gate.poisoned(cloned, digest()), Some(inc));
assert_eq!(
gate.admit(cloned, digest(), inc, 999, fp(0xA2)),
Admission::PoisonedIncarnation,
);
assert!(gate
.admit(cloned, digest(), Incarnation::new(8), 1, fp(0xC1))
.is_admitted());
}
}