use std::collections::{BTreeSet, HashMap};
use crate::id::{ConversationSeq, CorrelationId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExchangeState {
Pending,
Replied,
Elapsed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReplyClass {
First,
Duplicate,
Late,
Foreign,
}
#[derive(Debug, Default)]
pub(crate) struct ExchangeRegistry {
states: HashMap<CorrelationId, ExchangeState>,
}
impl ExchangeRegistry {
pub(crate) fn open(&mut self, correlation: CorrelationId) {
self.states.insert(correlation, ExchangeState::Pending);
}
pub(crate) fn classify_reply(&mut self, correlation: CorrelationId) -> ReplyClass {
match self.states.get(&correlation) {
Some(ExchangeState::Pending) => {
self.states.insert(correlation, ExchangeState::Replied);
ReplyClass::First
}
Some(ExchangeState::Replied) => ReplyClass::Duplicate,
Some(ExchangeState::Elapsed) => ReplyClass::Late,
None => ReplyClass::Foreign,
}
}
pub(crate) fn close_elapsed(&mut self, correlation: CorrelationId) {
if self.states.get(&correlation) == Some(&ExchangeState::Pending) {
self.states.insert(correlation, ExchangeState::Elapsed);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SeqGap {
pub(crate) expected: ConversationSeq,
pub(crate) observed: ConversationSeq,
}
pub(crate) const MAX_OWN_AHEAD: usize = 512;
#[derive(Debug, Default)]
pub(crate) struct CursorTracker {
next_expected: Option<u64>,
own_ahead: BTreeSet<u64>,
}
impl CursorTracker {
pub(crate) fn observe(&mut self, seq: ConversationSeq) -> Option<SeqGap> {
let value = seq.value();
match self.next_expected {
None => {
self.advance_past(value);
None
}
Some(expected) if value == expected => {
self.advance_past(value);
None
}
Some(expected) if value > expected => {
let hole = (expected..value).find(|position| !self.own_ahead.contains(position));
self.advance_past(value);
hole.map(|position| SeqGap {
expected: ConversationSeq::new(position),
observed: seq,
})
}
Some(_) => None,
}
}
pub(crate) fn observe_own(&mut self, seq: ConversationSeq) -> Option<SeqGap> {
let value = seq.value();
match self.next_expected {
None => {
self.advance_past(value);
None
}
Some(expected) if value == expected => {
self.advance_past(value);
None
}
Some(expected) if value > expected => {
self.own_ahead.insert(value);
if self.own_ahead.len() > MAX_OWN_AHEAD {
self.advance_past(value);
return Some(SeqGap {
expected: ConversationSeq::new(expected),
observed: seq,
});
}
None
}
Some(_) => None,
}
}
fn advance_past(&mut self, value: u64) {
let mut next = value.saturating_add(1);
self.own_ahead = self.own_ahead.split_off(&next);
while self.own_ahead.remove(&next) {
next = next.saturating_add(1);
}
self.next_expected = Some(next);
}
#[cfg(test)]
pub(crate) fn own_ahead_len(&self) -> usize {
self.own_ahead.len()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
#[test]
fn first_reply_is_first_then_duplicate() {
let mut registry = ExchangeRegistry::default();
let correlation = CorrelationId::mint();
registry.open(correlation);
assert_eq!(registry.classify_reply(correlation), ReplyClass::First);
assert_eq!(registry.classify_reply(correlation), ReplyClass::Duplicate);
}
#[test]
fn reply_after_elapse_is_late_and_stays_late() {
let mut registry = ExchangeRegistry::default();
let correlation = CorrelationId::mint();
registry.open(correlation);
registry.close_elapsed(correlation);
assert_eq!(registry.classify_reply(correlation), ReplyClass::Late);
assert_eq!(registry.classify_reply(correlation), ReplyClass::Late);
}
#[test]
fn elapse_after_reply_does_not_reopen_the_exchange() {
let mut registry = ExchangeRegistry::default();
let correlation = CorrelationId::mint();
registry.open(correlation);
assert_eq!(registry.classify_reply(correlation), ReplyClass::First);
registry.close_elapsed(correlation);
assert_eq!(registry.classify_reply(correlation), ReplyClass::Duplicate);
}
#[test]
fn unissued_correlation_is_foreign() {
let mut registry = ExchangeRegistry::default();
assert_eq!(
registry.classify_reply(CorrelationId::mint()),
ReplyClass::Foreign
);
}
#[test]
fn own_receipt_racing_held_foreign_delivery_is_not_a_gap() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(2)), None);
assert_eq!(
tracker.observe_own(ConversationSeq::new(4)),
None,
"an own receipt ahead of a held foreign delivery is not a hole"
);
assert_eq!(tracker.observe(ConversationSeq::new(3)), None);
assert_eq!(tracker.own_ahead_len(), 0);
assert_eq!(tracker.observe(ConversationSeq::new(5)), None);
}
#[test]
fn contiguous_own_commits_advance_without_ledger_growth() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe_own(ConversationSeq::new(2)), None);
assert_eq!(tracker.observe_own(ConversationSeq::new(3)), None);
assert_eq!(tracker.observe_own(ConversationSeq::new(4)), None);
assert_eq!(tracker.own_ahead_len(), 0);
assert_eq!(
tracker.observe(ConversationSeq::new(7)),
Some(SeqGap {
expected: ConversationSeq::new(5),
observed: ConversationSeq::new(7),
})
);
}
#[test]
fn true_foreign_hole_below_an_own_position_still_gaps() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(2)), None);
assert_eq!(tracker.observe_own(ConversationSeq::new(5)), None);
assert_eq!(
tracker.observe(ConversationSeq::new(6)),
Some(SeqGap {
expected: ConversationSeq::new(3),
observed: ConversationSeq::new(6),
})
);
assert_eq!(tracker.own_ahead_len(), 0);
assert_eq!(tracker.observe(ConversationSeq::new(7)), None);
}
#[test]
fn skip_of_exclusively_own_positions_is_not_a_hole() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(2)), None);
assert_eq!(tracker.observe_own(ConversationSeq::new(3)), None);
assert_eq!(tracker.observe_own(ConversationSeq::new(4)), None);
assert_eq!(tracker.observe(ConversationSeq::new(5)), None);
let mut parked = CursorTracker::default();
assert_eq!(parked.observe(ConversationSeq::new(10)), None);
assert_eq!(parked.observe_own(ConversationSeq::new(12)), None);
assert_eq!(parked.observe_own(ConversationSeq::new(13)), None);
assert_eq!(parked.own_ahead_len(), 2);
assert_eq!(parked.observe(ConversationSeq::new(11)), None);
assert_eq!(parked.own_ahead_len(), 0);
assert_eq!(parked.observe(ConversationSeq::new(14)), None);
}
#[test]
fn own_ahead_overflow_is_a_loud_gap_never_unbounded_memory() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(1)), None);
let mut fired = None;
for offset in 0..=u64::try_from(MAX_OWN_AHEAD).expect("cap fits u64") {
if let Some(gap) = tracker.observe_own(ConversationSeq::new(3 + offset)) {
fired = Some((offset, gap));
break;
}
}
let (at, gap) = fired.expect("the cap must fire before unbounded growth");
assert_eq!(
gap.expected,
ConversationSeq::new(2),
"the overflow gap names the stalled foreign position"
);
assert_eq!(
usize::try_from(at).expect("fits usize"),
MAX_OWN_AHEAD,
"the ledger held exactly the cap before converting to the typed gap"
);
assert!(tracker.own_ahead_len() <= MAX_OWN_AHEAD);
let next = 3 + at + 1;
assert_eq!(tracker.observe(ConversationSeq::new(next)), None);
}
#[test]
fn contiguous_sequences_raise_no_gap() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(3)), None);
assert_eq!(tracker.observe(ConversationSeq::new(4)), None);
assert_eq!(tracker.observe(ConversationSeq::new(5)), None);
}
#[test]
fn membership_forward_baseline_is_first_observation() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(41)), None);
assert_eq!(tracker.observe(ConversationSeq::new(42)), None);
}
#[test]
fn forward_skip_is_a_typed_gap() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(1)), None);
assert_eq!(
tracker.observe(ConversationSeq::new(4)),
Some(SeqGap {
expected: ConversationSeq::new(2),
observed: ConversationSeq::new(4),
})
);
assert_eq!(tracker.observe(ConversationSeq::new(5)), None);
}
#[test]
fn replayed_positions_are_not_gaps() {
let mut tracker = CursorTracker::default();
assert_eq!(tracker.observe(ConversationSeq::new(6)), None);
assert_eq!(tracker.observe(ConversationSeq::new(7)), None);
assert_eq!(tracker.observe(ConversationSeq::new(7)), None);
}
}