use crate::ipc::{Delta, IpcMessage, OutboxAck, WireStamp};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResyncAction {
Apply,
RequestSnapshot {
from_epoch: u64,
},
Ignore,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ResyncCoordinator {
last_epoch: u64,
resyncing: bool,
}
impl ResyncCoordinator {
pub fn new() -> Self {
Self::default()
}
pub fn with_epoch(last_epoch: u64) -> Self {
Self {
last_epoch,
resyncing: false,
}
}
pub fn last_epoch(&self) -> u64 {
self.last_epoch
}
pub fn is_resyncing(&self) -> bool {
self.resyncing
}
pub fn ingest_delta(&mut self, delta: &Delta) -> ResyncAction {
if delta.base_epoch == self.last_epoch {
if delta.epoch >= delta.base_epoch.saturating_add(1) {
self.last_epoch = delta.epoch;
self.resyncing = false;
ResyncAction::Apply
} else {
ResyncAction::Ignore
}
} else if delta.base_epoch < self.last_epoch {
ResyncAction::Ignore
} else {
if self.resyncing {
ResyncAction::Ignore
} else {
self.resyncing = true;
ResyncAction::RequestSnapshot {
from_epoch: self.last_epoch,
}
}
}
}
pub fn ingest_snapshot(&mut self, snapshot_epoch: u64) -> ResyncAction {
self.last_epoch = snapshot_epoch;
self.resyncing = false;
ResyncAction::Apply
}
pub fn ingest(&mut self, msg: &IpcMessage) -> ResyncAction {
match msg {
IpcMessage::Snapshot(s) => self.ingest_snapshot(s.epoch),
IpcMessage::Delta(d) => self.ingest_delta(d),
IpcMessage::CrdtSync(_) | IpcMessage::ResyncRequest(_) | IpcMessage::OutboxAck(_) => {
ResyncAction::Ignore
}
}
}
pub fn ack(&self) -> IpcMessage {
IpcMessage::OutboxAck(OutboxAck {
through_epoch: self.last_epoch,
})
}
}
pub trait DurableOutbox {
fn append(&mut self, epoch: u64, msg: IpcMessage);
fn ack_through(&mut self, epoch: u64);
fn replay_from(&self, cursor: u64) -> Vec<(u64, IpcMessage)>;
fn retained_epochs(&self) -> Vec<u64>;
}
#[derive(Debug, Clone, Default)]
pub struct InMemoryOutbox {
entries: Vec<(u64, IpcMessage)>,
acked_through: u64,
}
impl InMemoryOutbox {
pub fn new() -> Self {
Self::default()
}
pub fn acked_through(&self) -> u64 {
self.acked_through
}
}
impl DurableOutbox for InMemoryOutbox {
fn append(&mut self, epoch: u64, msg: IpcMessage) {
self.entries.push((epoch, msg));
}
fn ack_through(&mut self, epoch: u64) {
if epoch > self.acked_through {
self.acked_through = epoch;
}
self.entries.retain(|(e, _)| *e > self.acked_through);
}
fn replay_from(&self, cursor: u64) -> Vec<(u64, IpcMessage)> {
let mut out: Vec<(u64, IpcMessage)> = self
.entries
.iter()
.filter(|(e, _)| *e > cursor)
.cloned()
.collect();
out.sort_by_key(|(e, _)| *e);
out
}
fn retained_epochs(&self) -> Vec<u64> {
let mut es: Vec<u64> = self.entries.iter().map(|(e, _)| *e).collect();
es.sort_unstable();
es
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OrSet {
adds: BTreeSet<String>,
removes: BTreeSet<String>,
}
impl OrSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, tag: impl Into<String>) {
self.adds.insert(tag.into());
}
pub fn remove_observed<I, S>(&mut self, tags: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for t in tags {
self.removes.insert(t.into());
}
}
pub fn present(&self) -> bool {
self.adds.difference(&self.removes).next().is_some()
}
pub fn join(&mut self, other: &OrSet) {
self.adds.extend(other.adds.iter().cloned());
self.removes.extend(other.removes.iter().cloned());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireLwwRegister<V> {
stamp: WireStamp,
value: V,
}
impl<V: Clone> WireLwwRegister<V> {
pub fn new(stamp: WireStamp, value: V) -> Self {
Self { stamp, value }
}
pub fn value(&self) -> &V {
&self.value
}
pub fn stamp(&self) -> WireStamp {
self.stamp
}
pub fn set(&mut self, stamp: WireStamp, value: V) {
if stamp > self.stamp {
self.stamp = stamp;
self.value = value;
}
}
pub fn join(&mut self, other: &WireLwwRegister<V>) {
if other.stamp > self.stamp {
self.stamp = other.stamp;
self.value = other.value.clone();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ipc::Delta;
fn st(w: u64) -> WireStamp {
WireStamp {
wall_time: w,
logical: 0,
peer: 1,
}
}
#[test]
fn coordinator_applies_contiguous_and_advances() {
let mut c = ResyncCoordinator::with_epoch(40);
assert_eq!(
c.ingest_delta(&Delta::new(40, 41, vec![])),
ResyncAction::Apply
);
assert_eq!(c.last_epoch(), 41);
assert_eq!(
c.ingest_delta(&Delta::new(41, 44, vec![])),
ResyncAction::Apply
);
assert_eq!(c.last_epoch(), 44);
}
#[test]
fn coordinator_ignores_empty_backward_delta() {
let mut c = ResyncCoordinator::with_epoch(40);
assert_eq!(
c.ingest_delta(&Delta::new(40, 40, vec![])),
ResyncAction::Ignore
);
assert_eq!(c.last_epoch(), 40);
}
#[test]
fn coordinator_gap_requests_once_then_ignores() {
let mut c = ResyncCoordinator::with_epoch(2);
assert_eq!(
c.ingest_delta(&Delta::new(3, 4, vec![])),
ResyncAction::RequestSnapshot { from_epoch: 2 }
);
assert!(c.is_resyncing());
assert_eq!(
c.ingest_delta(&Delta::new(4, 5, vec![])),
ResyncAction::Ignore
);
assert_eq!(c.ingest_snapshot(5), ResyncAction::Apply);
assert!(!c.is_resyncing());
assert_eq!(c.last_epoch(), 5);
}
#[test]
fn ack_carries_last_epoch() {
let c = ResyncCoordinator::with_epoch(7);
assert_eq!(
c.ack(),
IpcMessage::OutboxAck(OutboxAck { through_epoch: 7 })
);
}
#[test]
fn outbox_retains_unacked_and_replays_from_cursor() {
let mut o = InMemoryOutbox::new();
for e in 41..=43 {
o.append(e, IpcMessage::Delta(Delta::new(e - 1, e, vec![])));
}
o.ack_through(41);
assert_eq!(o.retained_epochs(), vec![42, 43]);
let replay: Vec<u64> = o.replay_from(41).iter().map(|(e, _)| *e).collect();
assert_eq!(replay, vec![42, 43]);
}
#[test]
fn orset_join_is_commutative_and_add_wins() {
let mut a = OrSet::new();
a.add("t1");
let mut b = OrSet::new();
b.remove_observed(["t1"]);
b.add("t3"); let mut ab = a.clone();
ab.join(&b);
let mut ba = b.clone();
ba.join(&a);
assert_eq!(ab, ba, "join is commutative");
assert!(ab.present(), "add tag t3 not shadowed → present");
}
#[test]
fn lww_join_keeps_higher_stamp() {
let mut a = WireLwwRegister::new(st(10), true);
let b = WireLwwRegister::new(st(20), false);
a.join(&b);
assert!(!(*a.value()));
a.join(&WireLwwRegister::new(st(5), true));
assert!(!(*a.value()));
}
}