use crate::checkpoint::{resume_anchored, AnchorError, Checkpoint, CheckpointError};
use crate::fold::{fold_onto, state_hash, FoldedRecord, SyncState};
use crate::journal::OplogJournal;
use crate::lease::{Intent, IntentStatus, LeaseCoordinator, LeaseError};
use crate::oplog::{verify_log, ChainError, DeviceLog, Hlc, OpRecord, Scope, Surface, WallClock};
use crate::relay::{checkpoint_frontier, frontier_of, Frontier, Relay, RelayError};
use serde_json::Value;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum SessionError {
Io(std::io::Error),
Chain(ChainError),
Anchor(AnchorError),
Relay(RelayError),
Checkpoint(CheckpointError),
MissingCheckpoint { checkpoint_hash: String },
CheckpointRegression { held: String, offered: String },
Lease(LeaseError),
}
impl fmt::Display for SessionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionError::Io(e) => write!(f, "sync session io error: {e}"),
SessionError::Chain(e) => write!(f, "sync session chain error: {e}"),
SessionError::Anchor(e) => write!(f, "sync session anchor error: {e}"),
SessionError::Relay(e) => write!(f, "sync session relay error: {e}"),
SessionError::Checkpoint(e) => write!(f, "sync session checkpoint error: {e}"),
SessionError::MissingCheckpoint { checkpoint_hash } => write!(
f,
"journal is truncated below checkpoint {checkpoint_hash}, which is not in the \
session checkpoint directory — fetch it (relay checkpoint_get) and retry"
),
SessionError::CheckpointRegression { held, offered } => write!(
f,
"relay's latest checkpoint {offered} does not cover the session's base {held} — \
refusing to rebase onto it (state would be lost)"
),
SessionError::Lease(e) => write!(f, "sync session lease error: {e}"),
}
}
}
impl std::error::Error for SessionError {}
impl From<std::io::Error> for SessionError {
fn from(e: std::io::Error) -> Self {
SessionError::Io(e)
}
}
impl From<ChainError> for SessionError {
fn from(e: ChainError) -> Self {
SessionError::Chain(e)
}
}
impl From<AnchorError> for SessionError {
fn from(e: AnchorError) -> Self {
SessionError::Anchor(e)
}
}
impl From<RelayError> for SessionError {
fn from(e: RelayError) -> Self {
SessionError::Relay(e)
}
}
impl From<CheckpointError> for SessionError {
fn from(e: CheckpointError) -> Self {
SessionError::Checkpoint(e)
}
}
impl From<LeaseError> for SessionError {
fn from(e: LeaseError) -> Self {
SessionError::Lease(e)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PumpReport {
pub pushed: usize,
pub push_deduped: usize,
pub folded: usize,
pub acked: Option<Hlc>,
}
pub struct SyncSession {
device_id: String,
device: DeviceLog,
journal: OplogJournal,
checkpoint_dir: PathBuf,
wall: WallClock,
ops: Vec<OpRecord>,
base: Option<Checkpoint>,
pushed_through: Option<u64>,
}
impl fmt::Debug for SyncSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SyncSession")
.field("device_id", &self.device_id)
.field("ops", &self.ops.len())
.field("base", &self.base.as_ref().map(|c| &c.checkpoint_hash))
.field("pushed_through", &self.pushed_through)
.finish_non_exhaustive()
}
}
impl SyncSession {
pub fn open(
device_id: impl Into<String>,
journal_path: &Path,
checkpoint_dir: &Path,
wall: WallClock,
) -> Result<Self, SessionError> {
let device_id = device_id.into();
let (marker, ops) = OplogJournal::load_with_marker(journal_path)?;
let journal = OplogJournal::open(journal_path)?;
let (device, base) = match marker {
Some(marker) => {
let path = checkpoint_dir
.join(format!("{}.checkpoint.json", marker.checkpoint_hash));
if !path.exists() {
return Err(SessionError::MissingCheckpoint {
checkpoint_hash: marker.checkpoint_hash,
});
}
let checkpoint = Checkpoint::load(&path)?;
let device = resume_anchored(device_id.clone(), &checkpoint, &ops)?;
(device, Some(checkpoint))
}
None => (DeviceLog::resume(device_id.clone(), &ops)?, None),
};
let mut session = Self {
device_id,
device,
journal,
checkpoint_dir: checkpoint_dir.to_path_buf(),
wall,
ops,
base,
pushed_through: None,
};
session.device.set_wall_clock(session.wall.clone());
Ok(session)
}
pub fn bootstrap(
device_id: impl Into<String>,
journal_path: &Path,
checkpoint_dir: &Path,
relay: &mut dyn Relay,
wall: WallClock,
) -> Result<Self, SessionError> {
let mut session = Self::open(device_id, journal_path, checkpoint_dir, wall)?;
session.rebase(relay)?;
Ok(session)
}
pub fn append(
&mut self,
scope: Scope,
surface: Surface,
payload: Value,
) -> Result<OpRecord, SessionError> {
let op = self.device.append(scope, surface, payload);
if let Err(e) = self.journal.append(&op) {
self.rebuild_device()?;
return Err(SessionError::Io(e));
}
self.ops.push(op.clone());
Ok(op)
}
pub fn committed_run(&self, agent_id: &str, run_id: &str) -> Option<FoldedRecord> {
self.state().committed_run(agent_id, run_id).cloned()
}
pub fn record_intent(
&mut self,
scope: Scope,
intent: &Intent,
) -> Result<Option<OpRecord>, SessionError> {
if intent.status != IntentStatus::Committed
&& self.committed_run(&intent.agent_id, &intent.run_id).is_some()
{
return Ok(None); }
Ok(Some(self.append(scope, Surface::Intent, intent.payload())?))
}
pub fn record_intent_if_current(
&mut self,
scope: Scope,
intent: &Intent,
coordinator: &mut dyn LeaseCoordinator,
) -> Result<Option<OpRecord>, SessionError> {
let current = coordinator.current(&intent.agent_id)?;
let still_ours = current
.as_ref()
.is_some_and(|l| l.holder == self.device_id && l.epoch == intent.epoch);
if !still_ours {
return Ok(None); }
self.record_intent(scope, intent) }
fn rebuild_device(&mut self) -> Result<(), SessionError> {
let mut device = match &self.base {
Some(checkpoint) => resume_anchored(self.device_id.clone(), checkpoint, &self.ops)?,
None => DeviceLog::resume(self.device_id.clone(), &self.ops)?,
};
device.set_wall_clock(self.wall.clone());
self.device = device;
Ok(())
}
fn held_frontier(&self) -> Frontier {
let mut frontier = self
.base
.as_ref()
.map(checkpoint_frontier)
.unwrap_or_default();
for (device, seq) in frontier_of(&self.ops) {
let entry = frontier.entry(device).or_insert(seq);
if seq > *entry {
*entry = seq;
}
}
frontier
}
fn ack_frontier(&self) -> Option<Hlc> {
let from_ops = self.ops.iter().map(|op| &op.hlc).max();
let from_base = self
.base
.as_ref()
.and_then(|c| c.frontier.values().map(|e| &e.hlc).max());
[from_ops, from_base].into_iter().flatten().max().cloned()
}
pub fn pump(&mut self, relay: &mut dyn Relay) -> Result<PumpReport, SessionError> {
let mut report = PumpReport::default();
let mut own: Vec<OpRecord> = self
.ops
.iter()
.filter(|op| {
op.device_id == self.device_id
&& self.pushed_through.is_none_or(|through| op.seq > through)
})
.cloned()
.collect();
own.sort_by_key(|op| op.seq);
if !own.is_empty() {
self.journal.sync()?;
let outcome = relay.push(&self.device_id, &own)?;
report.pushed = outcome.accepted;
report.push_deduped = outcome.deduped;
self.pushed_through = own.last().map(|op| op.seq);
}
let pulled = relay.pull(&self.device_id, &self.held_frontier())?;
let held: std::collections::BTreeSet<&str> =
self.ops.iter().map(|op| op.op_id.as_str()).collect();
let new_ops: Vec<OpRecord> = pulled
.ops
.into_iter()
.filter(|op| !held.contains(op.op_id.as_str()))
.collect();
if !new_ops.is_empty() {
let mut candidate = self.ops.clone();
candidate.extend(new_ops.iter().cloned());
match &self.base {
Some(checkpoint) => crate::checkpoint::verify_anchored(checkpoint, &candidate)?,
None => verify_log(&candidate)?,
}
drop(candidate);
for op in &new_ops {
self.journal.append(op)?;
self.ops.push(op.clone());
self.device.observe(&op.hlc);
report.folded += 1;
}
self.journal.sync()?;
}
if let Some(frontier) = self.ack_frontier() {
relay.ack(&self.device_id, frontier.clone())?;
report.acked = Some(frontier);
}
Ok(report)
}
pub fn rebase(&mut self, relay: &mut dyn Relay) -> Result<bool, SessionError> {
let Some(checkpoint) = relay.checkpoint_get()? else {
return Ok(false); };
if let Some(base) = &self.base {
if base.checkpoint_hash == checkpoint.checkpoint_hash {
return Ok(false); }
if !crate::relay::frontier_dominates(&checkpoint, base) {
return Err(SessionError::CheckpointRegression {
held: base.checkpoint_hash.clone(),
offered: checkpoint.checkpoint_hash.clone(),
});
}
}
let pulled = relay.pull(&self.device_id, &checkpoint_frontier(&checkpoint))?;
let mut tail: Vec<OpRecord> = pulled.ops;
let mut seen: std::collections::BTreeSet<String> =
tail.iter().map(|op| op.op_id.clone()).collect();
for op in &self.ops {
let covered = checkpoint
.frontier
.get(&op.device_id)
.is_some_and(|entry| op.seq <= entry.seq);
if !covered && seen.insert(op.op_id.clone()) {
tail.push(op.clone());
}
}
tail.sort_by(|a, b| (&a.hlc, &a.op_id).cmp(&(&b.hlc, &b.op_id)));
crate::checkpoint::verify_anchored(&checkpoint, &tail)?;
checkpoint.save(&self.checkpoint_dir)?;
self.journal.truncate_to(&tail, &checkpoint.checkpoint_hash)?;
let mut device = resume_anchored(self.device_id.clone(), &checkpoint, &tail)?;
device.set_wall_clock(self.wall.clone());
self.device = device;
self.ops = tail;
self.base = Some(checkpoint);
self.pushed_through = None;
Ok(true)
}
pub fn publish_checkpoint(
&mut self,
relay: &mut dyn Relay,
) -> Result<Option<Checkpoint>, SessionError> {
if self.base.is_some() {
return Ok(None);
}
let Some(frontier) = relay.stable_frontier()? else {
return Ok(None);
};
let below: Vec<OpRecord> = self
.ops
.iter()
.filter(|op| op.hlc <= frontier)
.cloned()
.collect();
if below.is_empty() {
return Ok(None);
}
let checkpoint = Checkpoint::from_ops(&below)?;
relay.checkpoint_put(&self.device_id, &checkpoint)?;
Ok(Some(checkpoint))
}
pub fn state(&self) -> SyncState {
let base = self
.base
.as_ref()
.map(|c| c.state.clone())
.unwrap_or_default();
fold_onto(&base, &self.ops)
}
pub fn state_hash(&self) -> String {
state_hash(&self.state())
}
pub fn device_id(&self) -> &str {
&self.device_id
}
pub fn ops(&self) -> &[OpRecord] {
&self.ops
}
pub fn base(&self) -> Option<&Checkpoint> {
self.base.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relay::{AckOutcome, GcReport, InMemoryRelay, PullResult, PushOutcome, RelayConfig, RosterEntry};
use serde_json::json;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
fn zero_wall() -> WallClock {
Arc::new(|| 0)
}
fn mem_relay() -> InMemoryRelay {
InMemoryRelay::new(RelayConfig::default(), zero_wall())
}
struct Dirs {
_tmp: tempfile::TempDir,
journal: std::path::PathBuf,
ckpts: std::path::PathBuf,
}
fn dirs() -> Dirs {
let tmp = tempfile::tempdir().unwrap();
let journal = tmp.path().join("oplog.jsonl");
let ckpts = tmp.path().join("checkpoints");
Dirs { _tmp: tmp, journal, ckpts }
}
fn open(device: &str, d: &Dirs) -> SyncSession {
SyncSession::open(device, &d.journal, &d.ckpts, zero_wall()).unwrap()
}
#[test]
fn two_devices_converge_through_the_relay_across_all_tiers() {
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
let mut b = open("mac-b", &db);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1", "v": 1})).unwrap();
a.append(Scope::Personal, Surface::Declagent, json!({"id": "milo", "owner": "a"})).unwrap();
a.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})).unwrap();
b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2", "v": 2})).unwrap();
b.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})).unwrap();
a.pump(&mut relay).unwrap();
let rb = b.pump(&mut relay).unwrap();
assert_eq!(rb.folded, 3, "b folded a's three ops");
b.append(Scope::Personal, Surface::Declagent, json!({"id": "milo", "owner": "b"})).unwrap();
b.pump(&mut relay).unwrap();
let ra = a.pump(&mut relay).unwrap();
assert_eq!(ra.folded, 3);
assert_eq!(a.state_hash(), b.state_hash(), "divergence invariant: same hash");
let state = a.state();
assert_eq!(
state.registries[&Surface::Declagent.tag()]["id:milo"].payload["owner"],
json!("b"),
"LWW resolved by the hybrid clock's causal order"
);
assert_eq!(
state.log_entries(&Surface::Routing.tag()).len(),
2,
"the observation multiset survived transport"
);
let ra = a.pump(&mut relay).unwrap();
let rb = b.pump(&mut relay).unwrap();
assert_eq!((ra.pushed, ra.folded), (0, 0));
assert_eq!((rb.pushed, rb.folded), (0, 0));
assert_eq!(a.state_hash(), b.state_hash());
}
#[test]
fn op_is_journal_durable_before_it_is_ever_transmitted() {
let mut relay = mem_relay();
let d = dirs();
{
let mut a = open("mac-a", &d);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"})).unwrap();
}
let mut a = open("mac-a", &d);
let next = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"})).unwrap();
assert_eq!(next.seq, 1);
a.pump(&mut relay).unwrap();
assert_eq!(relay.pull("x", &Frontier::new()).unwrap().ops.len(), 2);
verify_log(a.ops()).unwrap();
}
#[test]
fn ack_is_derived_from_journal_held_state_only() {
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"})).unwrap();
a.pump(&mut relay).unwrap();
let mut b = open("mac-b", &db);
let report = b.pump(&mut relay).unwrap();
let acked = report.acked.unwrap();
drop(b);
let on_disk = OplogJournal::load(&db.journal).unwrap();
assert_eq!(acked, on_disk.iter().map(|op| op.hlc.clone()).max().unwrap());
let roster: std::collections::BTreeMap<String, RosterEntry> = relay
.roster()
.unwrap()
.into_iter()
.map(|e| (e.device_id.clone(), e))
.collect();
assert_eq!(roster["mac-b"].acked.as_ref(), Some(&acked));
}
struct FlakyAckRelay<'a> {
inner: &'a mut dyn Relay,
fail_ack: Arc<AtomicBool>,
}
impl Relay for FlakyAckRelay<'_> {
fn register(&mut self, d: &str) -> Result<RosterEntry, RelayError> {
self.inner.register(d)
}
fn push(&mut self, d: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError> {
self.inner.push(d, ops)
}
fn pull(&mut self, d: &str, since: &Frontier) -> Result<PullResult, RelayError> {
self.inner.pull(d, since)
}
fn ack(&mut self, d: &str, frontier: Hlc) -> Result<AckOutcome, RelayError> {
if self.fail_ack.load(Ordering::SeqCst) {
return Err(RelayError::Io(std::io::Error::other("network down")));
}
self.inner.ack(d, frontier)
}
fn checkpoint_put(&mut self, d: &str, c: &Checkpoint) -> Result<bool, RelayError> {
self.inner.checkpoint_put(d, c)
}
fn checkpoint_get(&mut self) -> Result<Option<Checkpoint>, RelayError> {
self.inner.checkpoint_get()
}
fn roster(&mut self) -> Result<Vec<RosterEntry>, RelayError> {
self.inner.roster()
}
fn stable_frontier(&mut self) -> Result<Option<Hlc>, RelayError> {
self.inner.stable_frontier()
}
fn gc(&mut self) -> Result<GcReport, RelayError> {
self.inner.gc()
}
}
#[test]
fn crash_mid_pump_is_idempotent_at_every_step() {
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"})).unwrap();
a.pump(&mut relay).unwrap();
let _ = relay.pull("mac-b", &Frontier::new()).unwrap();
let roster: std::collections::BTreeMap<String, RosterEntry> = relay
.roster()
.unwrap()
.into_iter()
.map(|e| (e.device_id.clone(), e))
.collect();
assert_eq!(roster["mac-b"].acked, None, "merely-received is never acked");
let fail = Arc::new(AtomicBool::new(true));
let mut b = open("mac-b", &db);
b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"})).unwrap();
{
let mut flaky = FlakyAckRelay { inner: &mut relay, fail_ack: fail.clone() };
let err = b.pump(&mut flaky).unwrap_err();
assert!(matches!(err, SessionError::Relay(RelayError::Io(_))));
}
let on_disk = OplogJournal::load(&db.journal).unwrap();
assert_eq!(on_disk.len(), 2);
let roster: std::collections::BTreeMap<String, RosterEntry> = relay
.roster()
.unwrap()
.into_iter()
.map(|e| (e.device_id.clone(), e))
.collect();
assert_eq!(roster["mac-b"].acked, None);
drop(b);
fail.store(false, Ordering::SeqCst);
let mut b = open("mac-b", &db);
let report = b.pump(&mut relay).unwrap();
assert_eq!(report.folded, 0, "re-pull re-fold is a no-op by op_id dedup");
assert_eq!(report.pushed, 0, "re-push deduped relay-side");
assert_eq!(report.push_deduped, 1);
assert!(report.acked.is_some());
a.pump(&mut relay).unwrap();
assert_eq!(a.state_hash(), b.state_hash(), "convergence after every crash point");
}
#[test]
fn cold_bootstrap_goes_through_resume_anchored_and_stays_fenced() {
let mut relay = mem_relay();
let da = dirs();
let mut a = open("mac-a", &da);
for i in 0..4 {
a.append(Scope::Personal, Surface::Knowledge, json!({"id": format!("f{i}"), "timestamp": i})).unwrap();
}
a.pump(&mut relay).unwrap();
let ckpt = a.publish_checkpoint(&mut relay).unwrap().unwrap();
relay.gc().unwrap();
let db = dirs();
let mut b = SyncSession::bootstrap("mac-b", &db.journal, &db.ckpts, &mut relay, zero_wall())
.unwrap();
assert_eq!(b.base().unwrap().checkpoint_hash, ckpt.checkpoint_hash);
b.pump(&mut relay).unwrap();
assert_eq!(b.state_hash(), a.state_hash());
let err = OplogJournal::load(&db.journal).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let (marker, tail) = OplogJournal::load_with_marker(&db.journal).unwrap();
assert_eq!(marker.unwrap().checkpoint_hash, ckpt.checkpoint_hash);
drop(b);
let mut b = open("mac-b", &db);
let op = b.append(Scope::Personal, Surface::Knowledge, json!({"id": "from-b"})).unwrap();
assert!(op.hlc > tail.iter().map(|o| o.hlc.clone()).max().unwrap_or(Hlc {
wall_ms: 0,
counter: 0,
device_id: String::new()
}));
b.pump(&mut relay).unwrap();
a.pump(&mut relay).unwrap();
assert_eq!(a.state_hash(), b.state_hash());
}
#[test]
fn partial_fold_failure_is_retry_safe_and_never_bricks_the_journal() {
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"})).unwrap();
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"})).unwrap();
a.pump(&mut relay).unwrap();
let mut b = open("mac-b", &db);
b.journal.fail_append_after = Some(1);
let err = b.pump(&mut relay).unwrap_err();
assert!(matches!(err, SessionError::Io(_)), "got {err:?}");
assert_eq!(b.ops().len(), 1, "only the successfully-journaled fold is in self.ops");
let on_disk = OplogJournal::load(&db.journal).unwrap();
assert_eq!(on_disk.len(), 1);
assert_eq!(on_disk[0].op_id, b.ops()[0].op_id);
let report = b.pump(&mut relay).unwrap();
assert_eq!(report.folded, 1, "only the un-journaled op is folded on retry");
let on_disk = OplogJournal::load(&db.journal).unwrap();
assert_eq!(on_disk.len(), 2, "no duplicate line — the journal is not bricked");
verify_log(&on_disk).expect("no DuplicateSeq: the journal opens cleanly");
drop(b);
let b = open("mac-b", &db);
assert_eq!(a.state_hash(), b.state_hash());
}
#[test]
fn append_failure_rolls_the_chain_back() {
let d = dirs();
let mut a = open("mac-a", &d);
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"})).unwrap();
a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"})).unwrap();
let before = a.ops().to_vec();
a.rebuild_device().unwrap();
let next = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f3"})).unwrap();
assert_eq!(next.seq, 2);
assert_eq!(next.prev.as_deref(), Some(before[1].op_id.as_str()));
let mut all = before;
all.push(next);
verify_log(&all).unwrap();
}
#[test]
fn partition_both_write_intents_converge_to_the_higher_epoch_winner() {
use crate::lease::{InMemoryLeaseCoordinator, IntentStatus};
use std::sync::atomic::AtomicU64;
let coord_t = Arc::new(AtomicU64::new(0));
let reader = coord_t.clone();
let coord_wall: WallClock = Arc::new(move || reader.load(Ordering::SeqCst));
let mut coord = InMemoryLeaseCoordinator::new(coord_wall);
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
let mut b = open("mac-b", &db);
let run = car_proto::deterministic_run_id("milo", "3am digest", "occurrence-1");
let lease_a = coord.acquire("milo", "mac-a", 100).unwrap();
assert_eq!(lease_a.epoch, 1);
let recorded = a
.record_intent_if_current(
Scope::Personal,
&Intent::new("milo", &run, 1, IntentStatus::Pending),
&mut coord,
)
.unwrap();
assert!(recorded.is_some(), "mac-a holds the lease → intent recorded");
a.pump(&mut relay).unwrap();
b.pump(&mut relay).unwrap();
coord_t.store(200, Ordering::SeqCst); let lease_b = coord.acquire("milo", "mac-b", 100).unwrap();
assert_eq!((lease_b.epoch, lease_b.holder.as_str()), (2, "mac-b"));
b.record_intent_if_current(
Scope::Personal,
&Intent::new("milo", &run, 2, IntentStatus::Pending),
&mut coord,
)
.unwrap()
.expect("mac-b holds epoch 2");
let b_commit = b
.record_intent_if_current(
Scope::Personal,
&Intent::new("milo", &run, 2, IntentStatus::Committed),
&mut coord,
)
.unwrap()
.expect("mac-b holds epoch 2");
b.pump(&mut relay).unwrap();
a.pump(&mut relay).unwrap();
let a_zombie = a
.record_intent(
Scope::Personal,
&Intent::new("milo", &run, 1, IntentStatus::Committed),
)
.unwrap()
.expect("a committed write is recorded (not terminal-guarded)");
assert!(a_zombie.hlc > b_commit.hlc, "the zombie's write is later in HLC");
a.pump(&mut relay).unwrap();
b.pump(&mut relay).unwrap();
assert_eq!(a.state_hash(), b.state_hash(), "divergence invariant holds");
let state = a.state();
assert_eq!(state.intents["milo"].runs.len(), 1, "one who-holds record");
assert_eq!(state.fencing_epoch("milo"), Some(2));
let winner = state.intent("milo", &run).expect("run present");
assert_eq!(winner.op_id, b_commit.op_id, "higher-epoch commit wins despite lower HLC");
let decoded = Intent::from_payload(&winner.payload).unwrap();
assert_eq!((decoded.epoch, decoded.status), (2, IntentStatus::Committed));
assert_eq!(a.committed_run("milo", &run).unwrap().op_id, b_commit.op_id);
assert_eq!(b.committed_run("milo", &run).unwrap().op_id, b_commit.op_id);
}
#[test]
fn record_intent_if_current_gates_out_a_lost_holder() {
use crate::lease::{InMemoryLeaseCoordinator, IntentStatus};
let mut coord = InMemoryLeaseCoordinator::new(zero_wall());
let d = dirs();
let mut a = open("mac-a", &d);
let run = "run-x";
coord.acquire("milo", "mac-a", 100).unwrap();
let recorded = a
.record_intent_if_current(
Scope::Personal,
&Intent::new("milo", run, 1, IntentStatus::Pending),
&mut coord,
)
.unwrap();
assert!(recorded.is_some());
assert_eq!(a.ops().len(), 1);
coord.release("milo", "mac-a", 1).unwrap();
coord.acquire("milo", "mac-b", 100).unwrap();
let gated = a
.record_intent_if_current(
Scope::Personal,
&Intent::new("milo", run, 1, IntentStatus::Committed),
&mut coord,
)
.unwrap();
assert!(gated.is_none(), "coordinator says mac-a lost → not recorded");
assert_eq!(a.ops().len(), 1, "nothing new journaled");
}
#[test]
fn c3_record_intent_no_ops_a_pending_for_an_already_committed_run() {
let mut relay = mem_relay();
let (da, db) = (dirs(), dirs());
let mut a = open("mac-a", &da);
let mut b = open("mac-b", &db);
let run = "run-nightly";
a.record_intent(Scope::Personal, &Intent::new("milo", run, 1, IntentStatus::Committed))
.unwrap()
.expect("first commit recorded");
a.pump(&mut relay).unwrap();
b.pump(&mut relay).unwrap();
assert!(b.committed_run("milo", run).is_some(), "mac-b folded the commit");
let before = b.ops().len();
let attempt = b
.record_intent(Scope::Personal, &Intent::new("milo", run, 2, IntentStatus::Pending))
.unwrap();
assert!(attempt.is_none(), "pending for an already-committed run is a no-op");
assert_eq!(b.ops().len(), before, "nothing journaled");
b.pump(&mut relay).unwrap();
a.pump(&mut relay).unwrap();
assert!(a.committed_run("milo", run).is_some());
let decoded = Intent::from_payload(&b.state().intent("milo", run).unwrap().payload).unwrap();
assert_eq!(decoded.status, IntentStatus::Committed, "not reverted to pending");
}
}