use std::collections::{BTreeMap, BTreeSet};
use super::recovery::NOT_YET_APPLIED_EPOCH;
#[derive(Debug)]
pub struct AppliedGate {
fully_applied_epoch: u64,
applied_tail: BTreeSet<(u64, u32)>,
epoch_expected: BTreeMap<u64, u32>,
delivered_unknown: BTreeMap<u64, BTreeSet<u32>>,
highest_seen_epoch: u64,
}
impl AppliedGate {
pub fn new(fully_applied_epoch: u64, applied_tail: BTreeSet<(u64, u32)>) -> Self {
Self {
fully_applied_epoch,
applied_tail,
epoch_expected: BTreeMap::new(),
delivered_unknown: BTreeMap::new(),
highest_seen_epoch: NOT_YET_APPLIED_EPOCH,
}
}
pub fn fully_applied_epoch(&self) -> u64 {
self.fully_applied_epoch
}
fn is_sentinel(&self) -> bool {
self.fully_applied_epoch == NOT_YET_APPLIED_EPOCH
}
pub fn note_expected(&mut self, epoch: u64, position: u32, count: u32) {
if !self.is_sentinel() && epoch <= self.fully_applied_epoch {
return;
}
if count == 0 {
self.delivered_unknown
.entry(epoch)
.or_default()
.insert(position);
} else {
self.epoch_expected.entry(epoch).or_insert(count);
}
if self.highest_seen_epoch == NOT_YET_APPLIED_EPOCH || epoch > self.highest_seen_epoch {
self.highest_seen_epoch = epoch;
}
}
pub fn is_applied(&self, epoch: u64, position: u32) -> bool {
(!self.is_sentinel() && epoch <= self.fully_applied_epoch)
|| self.applied_tail.contains(&(epoch, position))
}
pub fn mark_applied(&mut self, epoch: u64, position: u32) -> Option<u64> {
if !self.is_sentinel() && epoch <= self.fully_applied_epoch {
return None;
}
self.applied_tail.insert((epoch, position));
self.advance()
}
pub fn advance(&mut self) -> Option<u64> {
let start = self.fully_applied_epoch;
if self.highest_seen_epoch == NOT_YET_APPLIED_EPOCH {
return None;
}
loop {
let next = if self.is_sentinel() {
match self.next_participating(0) {
Some(e) => e,
None => break,
}
} else {
let n = self.fully_applied_epoch + 1;
if n > self.highest_seen_epoch {
break;
}
n
};
if let Some(expected) = self.epoch_expected.get(&next).copied() {
if expected == 0 || self.epoch_applied_count(next) < expected {
break;
}
self.fold_epoch(next);
} else if self.delivered_unknown.contains_key(&next) {
if self.highest_seen_epoch <= next
|| self.epoch_applied_count(next) < self.delivered_count(next)
{
break;
}
self.fold_epoch(next);
} else {
match self.next_participating(next) {
Some(k) if k <= self.highest_seen_epoch => {
self.fully_applied_epoch = k - 1;
}
_ => {
self.fully_applied_epoch = self.highest_seen_epoch;
break;
}
}
}
}
if self.fully_applied_epoch != start {
Some(self.fully_applied_epoch)
} else {
None
}
}
fn epoch_applied_count(&self, epoch: u64) -> u32 {
self.applied_tail
.range((epoch, 0)..=(epoch, u32::MAX))
.count() as u32
}
fn delivered_count(&self, epoch: u64) -> u32 {
self.delivered_unknown
.get(&epoch)
.map(|positions| positions.len() as u32)
.unwrap_or(0)
}
fn next_participating(&self, from: u64) -> Option<u64> {
let known = self.epoch_expected.range(from..).next().map(|(k, _)| *k);
let unknown = self.delivered_unknown.range(from..).next().map(|(k, _)| *k);
match (known, unknown) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
fn fold_epoch(&mut self, epoch: u64) {
self.fully_applied_epoch = epoch;
self.epoch_expected.remove(&epoch);
self.delivered_unknown.remove(&epoch);
self.prune_epoch(epoch);
}
fn prune_epoch(&mut self, epoch: u64) {
let doomed: Vec<(u64, u32)> = self
.applied_tail
.range((epoch, 0)..=(epoch, u32::MAX))
.copied()
.collect();
for key in doomed {
self.applied_tail.remove(&key);
}
}
#[cfg(test)]
pub fn tail_len(&self) -> usize {
self.applied_tail.len()
}
#[cfg(test)]
pub fn has_epoch_state(&self, epoch: u64) -> bool {
self.epoch_expected.contains_key(&epoch)
|| self.delivered_unknown.contains_key(&epoch)
|| self
.applied_tail
.range((epoch, 0)..=(epoch, u32::MAX))
.next()
.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_gate() -> AppliedGate {
AppliedGate::new(NOT_YET_APPLIED_EPOCH, BTreeSet::new())
}
#[test]
fn per_position_skip_is_exact() {
let mut tail = BTreeSet::new();
tail.insert((5u64, 0u32));
let gate = AppliedGate::new(NOT_YET_APPLIED_EPOCH, tail);
assert!(
gate.is_applied(5, 0),
"(5,0) is applied and must be skipped"
);
assert!(
!gate.is_applied(5, 1),
"(5,1) is NOT applied and must NOT be skipped — else a torn txn"
);
}
#[test]
fn watermark_skips_at_or_below() {
let gate = AppliedGate::new(4, BTreeSet::new());
assert!(gate.is_applied(3, 9), "epoch below W is applied");
assert!(gate.is_applied(4, 0), "epoch at W is applied");
assert!(!gate.is_applied(5, 0), "epoch above W is not applied by W");
}
#[test]
fn sentinel_never_skips_by_watermark() {
let gate = empty_gate();
assert!(!gate.is_applied(0, 0), "sentinel W must not skip epoch 0");
}
#[test]
fn full_epoch_folds_and_prunes_tail() {
let mut gate = empty_gate();
gate.note_expected(0, 0, 2);
assert_eq!(gate.mark_applied(0, 0), None, "1 of 2 applied — no advance");
assert_eq!(gate.fully_applied_epoch(), NOT_YET_APPLIED_EPOCH);
assert_eq!(gate.tail_len(), 1);
assert_eq!(gate.mark_applied(0, 1), Some(0), "2 of 2 applied — advance");
assert_eq!(gate.fully_applied_epoch(), 0);
assert_eq!(gate.tail_len(), 0, "folded epoch's tail entries are pruned");
}
#[test]
fn partial_epoch_does_not_advance() {
let mut gate = empty_gate();
gate.note_expected(3, 0, 3);
gate.mark_applied(3, 0);
gate.mark_applied(3, 2);
assert_eq!(
gate.fully_applied_epoch(),
NOT_YET_APPLIED_EPOCH,
"2 of 3 positions applied must not fold the epoch"
);
assert_eq!(gate.tail_len(), 2);
}
#[test]
fn advance_walks_contiguous_epochs_and_stops_at_gap() {
let mut gate = empty_gate();
gate.note_expected(0, 0, 1);
gate.note_expected(1, 0, 1);
gate.note_expected(2, 0, 2);
gate.mark_applied(2, 0);
assert_eq!(gate.fully_applied_epoch(), NOT_YET_APPLIED_EPOCH);
gate.mark_applied(1, 0);
assert_eq!(gate.fully_applied_epoch(), NOT_YET_APPLIED_EPOCH);
assert_eq!(gate.mark_applied(0, 0), Some(1));
assert_eq!(gate.fully_applied_epoch(), 1);
assert_eq!(gate.tail_len(), 1, "only the incomplete epoch 2 remains");
assert_eq!(gate.mark_applied(2, 1), Some(2));
assert_eq!(gate.fully_applied_epoch(), 2);
assert_eq!(gate.tail_len(), 0);
}
#[test]
fn watermark_skips_non_participating_epochs() {
let mut gate = empty_gate();
gate.note_expected(0, 0, 1);
gate.note_expected(2, 0, 1);
assert_eq!(gate.mark_applied(0, 0), Some(1));
assert_eq!(
gate.fully_applied_epoch(),
1,
"watermark jumps over non-participating epoch 1"
);
assert_eq!(gate.mark_applied(2, 0), Some(2));
assert_eq!(gate.fully_applied_epoch(), 2);
assert_eq!(gate.tail_len(), 0, "no tail entries leak across the gap");
}
#[test]
fn watermark_does_not_advance_past_highest_delivered() {
let mut gate = empty_gate();
gate.note_expected(0, 0, 1);
assert_eq!(gate.mark_applied(0, 0), Some(0));
assert_eq!(
gate.fully_applied_epoch(),
0,
"watermark stops at the highest delivered epoch, not beyond"
);
}
#[test]
fn recovered_prefix_folds_when_counts_are_learned() {
let mut tail = BTreeSet::new();
tail.insert((5u64, 0u32));
tail.insert((5u64, 1u32));
let mut gate = AppliedGate::new(NOT_YET_APPLIED_EPOCH, tail);
assert_eq!(gate.tail_len(), 2);
gate.note_expected(5, 0, 2);
assert_eq!(gate.advance(), Some(5));
assert_eq!(gate.fully_applied_epoch(), 5);
assert_eq!(gate.tail_len(), 0, "recovered prefix is reclaimed");
assert!(gate.is_applied(5, 0));
assert!(gate.is_applied(5, 1));
}
#[test]
fn recovered_epoch_with_missing_position_reprocesses_it() {
let mut tail = BTreeSet::new();
tail.insert((7u64, 0u32));
let mut gate = AppliedGate::new(NOT_YET_APPLIED_EPOCH, tail);
gate.note_expected(7, 0, 2);
assert!(gate.is_applied(7, 0), "committed position stays skipped");
assert!(
!gate.is_applied(7, 1),
"in-flight position must be re-processed, not skipped"
);
assert_eq!(gate.advance(), None);
assert_eq!(gate.fully_applied_epoch(), NOT_YET_APPLIED_EPOCH);
assert_eq!(gate.mark_applied(7, 1), Some(7), "now the epoch folds");
assert_eq!(gate.tail_len(), 0);
}
#[test]
fn unknown_count_epoch_folds_once_a_higher_epoch_is_seen() {
let mut gate = empty_gate();
gate.note_expected(5, 0, 0);
gate.note_expected(5, 1, 0);
assert_eq!(
gate.mark_applied(5, 0),
None,
"unknown count, still highest"
);
assert_eq!(
gate.mark_applied(5, 1),
None,
"both applied but count unknown"
);
assert_eq!(
gate.fully_applied_epoch(),
NOT_YET_APPLIED_EPOCH,
"unknown-count epoch must not fold while it is the highest seen"
);
gate.note_expected(6, 0, 0);
assert_eq!(gate.advance(), Some(5), "seeing epoch 6 folds epoch 5");
assert_eq!(gate.fully_applied_epoch(), 5);
assert!(
!gate.has_epoch_state(5),
"folded epoch leaks no tail/expected/delivered entries"
);
assert_eq!(gate.tail_len(), 0, "epoch 5's tail positions are pruned");
}
#[test]
fn unknown_count_epoch_does_not_fold_with_unapplied_position() {
let mut gate = empty_gate();
gate.note_expected(5, 0, 0);
gate.note_expected(5, 1, 0);
gate.mark_applied(5, 0);
gate.note_expected(6, 0, 0);
assert_eq!(gate.advance(), None, "unapplied position blocks the fold");
assert_eq!(
gate.fully_applied_epoch(),
NOT_YET_APPLIED_EPOCH,
"watermark stays below an epoch with an unapplied delivered position"
);
assert!(gate.is_applied(5, 0), "applied position stays skipped");
assert!(
!gate.is_applied(5, 1),
"unapplied position must be re-processed, not skipped"
);
}
#[test]
fn known_count_epoch_still_folds_immediately() {
let mut gate = empty_gate();
gate.note_expected(4, 0, 2);
gate.note_expected(4, 1, 2);
assert_eq!(gate.mark_applied(4, 0), None, "1 of 2 applied — no advance");
assert_eq!(
gate.mark_applied(4, 1),
Some(4),
"2 of 2 applied — folds immediately, no higher epoch required"
);
assert_eq!(gate.fully_applied_epoch(), 4);
assert!(
!gate.has_epoch_state(4),
"folded epoch leaks no tail/expected/delivered entries"
);
}
}