use crate::ipc::{Delta, IpcMessage, IpcSink, IpcSource, OutboxAck, ResyncRequest, WireStamp};
use std::collections::{BTreeSet, VecDeque};
#[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();
}
}
}
pub trait Clock {
fn now_millis(&self) -> u64;
}
pub trait SnapshotProvider {
fn snapshot(&self, from_epoch: u64) -> IpcMessage;
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Progress {
pub sent: usize,
pub applied: Vec<IpcMessage>,
pub resync_requested: bool,
pub snapshots_served: usize,
pub peer_acked_through: u64,
pub retained: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DriverError<RE> {
Source(RE),
}
pub struct SyncDriver<S, R, O, C, P>
where
S: IpcSink,
R: IpcSource,
O: DurableOutbox,
C: Clock,
P: SnapshotProvider,
{
sink: S,
source: R,
outbox: O,
clock: C,
provider: P,
coordinator: ResyncCoordinator,
pending: VecDeque<(u64, IpcMessage)>,
peer_acked_through: u64,
ack_owed: bool,
replay_pending: bool,
stalled_since: Option<u64>,
}
impl<S, R, O, C, P> SyncDriver<S, R, O, C, P>
where
S: IpcSink,
R: IpcSource,
O: DurableOutbox,
C: Clock,
P: SnapshotProvider,
{
pub fn new(sink: S, source: R, outbox: O, clock: C, provider: P) -> Self {
Self::with_epoch(sink, source, outbox, clock, provider, 0)
}
pub fn with_epoch(
sink: S,
source: R,
outbox: O,
clock: C,
provider: P,
last_epoch: u64,
) -> Self {
Self {
sink,
source,
outbox,
clock,
provider,
coordinator: ResyncCoordinator::with_epoch(last_epoch),
pending: VecDeque::new(),
peer_acked_through: 0,
ack_owed: false,
replay_pending: false,
stalled_since: None,
}
}
pub fn enqueue(&mut self, epoch: u64, msg: IpcMessage) {
self.pending.push_back((epoch, msg));
}
pub fn on_reconnect(&mut self) {
self.replay_pending = true;
self.ack_owed = true;
self.stalled_since = None;
}
pub fn last_epoch(&self) -> u64 {
self.coordinator.last_epoch()
}
pub fn is_stalled(&self) -> bool {
self.stalled_since.is_some()
}
pub fn stalled_for(&self, now: u64) -> u64 {
self.stalled_since
.map_or(0, |since| now.saturating_sub(since))
}
pub fn outbox(&self) -> &O {
&self.outbox
}
pub fn tick(&mut self) -> Result<Progress, DriverError<R::Error>> {
let now = self.clock.now_millis();
let mut progress = Progress::default();
if self.replay_pending {
self.replay_pending = false;
for (_, msg) in self.outbox.replay_from(self.peer_acked_through) {
if self.sink.send(&msg).is_ok() {
progress.sent += 1;
} else {
self.stalled_since = Some(now);
self.replay_pending = true; break;
}
}
}
while self.stalled_since.is_none() {
let Some((epoch, msg)) = self.pending.front().cloned() else {
break;
};
self.outbox.append(epoch, msg.clone());
self.pending.pop_front();
match self.sink.send(&msg) {
Ok(()) => {
progress.sent += 1;
self.stalled_since = None;
}
Err(_) => {
self.stalled_since = Some(now);
break;
}
}
}
loop {
let msg = match self.source.recv() {
Ok(Some(msg)) => msg,
Ok(None) => break,
Err(e) => return Err(DriverError::Source(e)),
};
match msg {
IpcMessage::OutboxAck(ack) => {
if ack.through_epoch > self.peer_acked_through {
self.peer_acked_through = ack.through_epoch;
}
self.outbox.ack_through(ack.through_epoch);
}
IpcMessage::ResyncRequest(req) => {
let snap = self.provider.snapshot(req.from_epoch);
if self.sink.send(&snap).is_ok() {
progress.snapshots_served += 1;
} else {
self.stalled_since = Some(now);
}
}
IpcMessage::CrdtSync(_) => {
progress.applied.push(msg);
}
IpcMessage::Snapshot(_) | IpcMessage::Delta(_) => {
match self.coordinator.ingest(&msg) {
ResyncAction::Apply => {
self.ack_owed = true;
progress.applied.push(msg);
}
ResyncAction::RequestSnapshot { from_epoch } => {
let req = IpcMessage::ResyncRequest(ResyncRequest { from_epoch });
if self.sink.send(&req).is_ok() {
progress.resync_requested = true;
} else {
self.stalled_since = Some(now);
}
}
ResyncAction::Ignore => {}
}
}
}
}
if self.ack_owed && self.sink.send(&self.coordinator.ack()).is_ok() {
self.ack_owed = false;
}
progress.peer_acked_through = self.peer_acked_through;
progress.retained = self.outbox.retained_epochs().len();
Ok(progress)
}
}
#[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()));
}
use crate::ipc::Snapshot;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Clone, Default)]
struct Wire {
sent: Rc<RefCell<Vec<IpcMessage>>>,
inbound: Rc<RefCell<VecDeque<IpcMessage>>>,
}
struct TestSink {
sent: Rc<RefCell<Vec<IpcMessage>>>,
up: Rc<Cell<bool>>,
}
struct TestSource {
inbound: Rc<RefCell<VecDeque<IpcMessage>>>,
err: Rc<Cell<bool>>,
}
impl IpcSink for TestSink {
type Error = ();
fn send(&mut self, message: &IpcMessage) -> Result<(), ()> {
if !self.up.get() {
return Err(());
}
self.sent.borrow_mut().push(message.clone());
Ok(())
}
}
impl IpcSource for TestSource {
type Error = ();
fn recv(&mut self) -> Result<Option<IpcMessage>, ()> {
if self.err.get() {
self.err.set(false);
return Err(());
}
Ok(self.inbound.borrow_mut().pop_front())
}
}
struct Zero;
impl Clock for Zero {
fn now_millis(&self) -> u64 {
0
}
}
struct SnapAhead;
impl SnapshotProvider for SnapAhead {
fn snapshot(&self, from_epoch: u64) -> IpcMessage {
IpcMessage::Snapshot(Snapshot::new(from_epoch + 5, vec![], vec![], vec![]))
}
}
type TestDriver = SyncDriver<TestSink, TestSource, InMemoryOutbox, Zero, SnapAhead>;
fn driver_at(last_epoch: u64) -> (Wire, Rc<Cell<bool>>, Rc<Cell<bool>>, TestDriver) {
let wire = Wire::default();
let up = Rc::new(Cell::new(true));
let err = Rc::new(Cell::new(false));
let sink = TestSink {
sent: wire.sent.clone(),
up: up.clone(),
};
let source = TestSource {
inbound: wire.inbound.clone(),
err: err.clone(),
};
let driver = SyncDriver::with_epoch(
sink,
source,
InMemoryOutbox::new(),
Zero,
SnapAhead,
last_epoch,
);
(wire, up, err, driver)
}
fn delta(base: u64, epoch: u64) -> IpcMessage {
IpcMessage::Delta(Delta::new(base, epoch, vec![]))
}
#[test]
fn driver_drains_append_before_send_and_retains_until_acked() {
let (wire, _up, _err, mut d) = driver_at(0);
d.enqueue(1, delta(0, 1));
d.enqueue(2, delta(1, 2));
let p = d.tick().unwrap();
assert_eq!(p.sent, 2, "both fresh frames pushed to the sink");
assert_eq!(wire.sent.borrow().len(), 2);
assert_eq!(p.retained, 2, "appended-before-send, retained until acked");
assert!(!d.is_stalled());
wire.inbound
.borrow_mut()
.push_back(IpcMessage::OutboxAck(OutboxAck { through_epoch: 2 }));
let p = d.tick().unwrap();
assert_eq!(p.peer_acked_through, 2);
assert_eq!(p.retained, 0, "acked frames pruned");
}
#[test]
fn driver_retains_on_send_failure_and_replays_on_reconnect() {
let (wire, up, _err, mut d) = driver_at(0);
up.set(false); d.enqueue(1, delta(0, 1));
let p = d.tick().unwrap();
assert_eq!(p.sent, 0);
assert!(d.is_stalled(), "a failed send stalls the driver");
assert_eq!(
p.retained, 1,
"frame retained in the outbox despite the failure"
);
assert!(wire.sent.borrow().is_empty());
assert_eq!(
d.stalled_for(250),
250,
"stall duration is a host backoff signal"
);
up.set(true);
d.on_reconnect();
let p = d.tick().unwrap();
assert!(!d.is_stalled());
assert_eq!(p.sent, 1, "the retained frame is replayed");
assert!(
wire.sent
.borrow()
.iter()
.any(|m| matches!(m, IpcMessage::Delta(dd) if dd.epoch == 1)),
"the replayed delta reached the sink"
);
}
#[test]
fn driver_applies_delta_and_advertises_receiver_cursor() {
let (wire, _up, _err, mut d) = driver_at(0);
wire.inbound.borrow_mut().push_back(delta(0, 1));
let p = d.tick().unwrap();
assert_eq!(
p.applied.len(),
1,
"the applied frame is handed to the host"
);
assert_eq!(d.last_epoch(), 1);
assert!(
wire.sent
.borrow()
.iter()
.any(|m| matches!(m, IpcMessage::OutboxAck(a) if a.through_epoch == 1)),
"an OutboxAck advertising the new cursor was sent"
);
}
#[test]
fn driver_redelivery_is_idempotent_no_op() {
let (wire, _up, _err, mut d) = driver_at(0);
wire.inbound.borrow_mut().push_back(delta(0, 1));
assert_eq!(d.tick().unwrap().applied.len(), 1);
wire.inbound.borrow_mut().push_back(delta(0, 1));
let p = d.tick().unwrap();
assert_eq!(p.applied.len(), 0, "already-applied re-delivery is ignored");
assert_eq!(d.last_epoch(), 1, "cursor does not double-advance");
}
#[test]
fn driver_requests_snapshot_on_inbound_gap() {
let (wire, _up, _err, mut d) = driver_at(2);
wire.inbound.borrow_mut().push_back(delta(3, 4)); let p = d.tick().unwrap();
assert!(p.resync_requested);
assert!(p.applied.is_empty(), "the gapped delta is not applied");
assert!(
wire.sent
.borrow()
.iter()
.any(|m| matches!(m, IpcMessage::ResyncRequest(r) if r.from_epoch == 2)),
"a ResyncRequest at the current cursor was emitted"
);
}
#[test]
fn driver_answers_resync_request_with_provider_snapshot() {
let (wire, _up, _err, mut d) = driver_at(0);
wire.inbound
.borrow_mut()
.push_back(IpcMessage::ResyncRequest(ResyncRequest { from_epoch: 2 }));
let p = d.tick().unwrap();
assert_eq!(p.snapshots_served, 1);
assert!(
wire.sent
.borrow()
.iter()
.any(|m| matches!(m, IpcMessage::Snapshot(s) if s.epoch == 7)),
"a covering snapshot (from_epoch + 5) was sent"
);
}
#[test]
fn driver_surfaces_source_read_error() {
let (_wire, _up, err, mut d) = driver_at(0);
err.set(true);
assert_eq!(d.tick(), Err(DriverError::Source(())));
}
#[test]
fn driver_gap_then_snapshot_converges() {
let (wire, _up, _err, mut d) = driver_at(2);
wire.inbound.borrow_mut().push_back(delta(4, 5)); d.tick().unwrap();
assert_eq!(d.last_epoch(), 2, "still stuck at the pre-gap cursor");
wire.inbound
.borrow_mut()
.push_back(IpcMessage::Snapshot(Snapshot::new(
5,
vec![],
vec![],
vec![],
)));
let p = d.tick().unwrap();
assert_eq!(p.applied.len(), 1);
assert_eq!(d.last_epoch(), 5, "snapshot restored convergence");
}
}