From 01db6c0b71a576bb17c1f62a5ae46d75efb324f9 Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Wed, 21 Oct 2026 22:38:47 -0700 Subject: [PATCH] docs(core): rewrite flush, WallClockStore and age_ms rustdoc from the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Store::flush and Node::flush: the §4 contract (durability point, time anchor, harmless twice, earlier now clamped, Err(Backend) leaves the store usable, application decides when). No "infallible" or "duplicate custodian" wording. - WallClockStore and Node::set_wall_clock / peer_time_estimate: define quarantine generically, a second set_wall_clock is a no-op returning the first report, peer time is unauthenticated until 1.1. - Observation::age_ms: a lower bound that every holder advances; use observed_at for absolute time. Co-Authored-By: Claude Opus 5.5 --- tines-core/src/node.rs | 31 ++++++++++------ tines-core/src/observation.rs | 16 +++++---- tines-core/src/store.rs | 67 ++++++++++++++++++++++++----------- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/tines-core/src/node.rs b/tines-core/src/node.rs index 3187e729..41eaec74 100644 --- a/tines-core/src/node.rs +++ b/tines-core/src/node.rs @@ -219,8 +219,13 @@ impl Node { &self.store } - /// Lift the quarantine on held observations once the host has trusted wall - /// time, by folding the wall clock into a [`WallClockStore`] backend. + /// Pass trusted wall time (ms since the Unix epoch) to a + /// [`WallClockStore`] backend, which folds the downtime into what it held + /// at start-up and lifts its quarantine (see [`WallClockStore`]). + /// + /// Call it once, when the host first has trusted time. A second call is a + /// no-op that returns the first fold's report. Core forwards the value and + /// never reads it. pub fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> ::FoldReport where S: WallClockStore, @@ -228,8 +233,11 @@ impl Node { self.store.set_wall_clock(wall_now, now) } - /// Best estimate of trusted wall time from a peer's observation, if the - /// backend supports it (see [`WallClockStore`]). + /// The backend's estimate of the current wall time from peers' + /// observations, if any (see [`WallClockStore::peer_time_estimate`]). + /// + /// Peer time is unauthenticated until 1.1: bound the estimate before + /// passing it to [`Self::set_wall_clock`]. pub fn peer_time_estimate(&self, now: Tick) -> Option where S: WallClockStore, @@ -237,13 +245,16 @@ impl Node { self.store.peer_time_estimate(now) } - /// Best-effort durability flush, advancing each stored observation's age - /// up to `now`. + /// Flush the store: its durability point (see [`Store::flush`]). /// - /// This is the durability boundary a host reaches on its flush schedule — - /// delegates to [`Store::flush`], so it is infallible by contract: a failed - /// flush is, at worst, a duplicate custodian on the next exchange, never a - /// lost message. + /// Everything inserted, released or trimmed so far becomes durable, and + /// `now` becomes the time anchor for ages after a restart. Flushing twice + /// is harmless, and a `now` earlier than the last anchor is clamped to it. + /// A failure returns [`StoreError::Backend`] and leaves the node and its + /// store usable, so the application can retry on its next flush. + /// + /// Core never flushes on its own: the application decides how often, + /// like `poll` and `trim`. On flash, every flush costs wear. pub fn flush(&mut self, now: Tick) -> Result<(), StoreError> { self.store.flush(now) } diff --git a/tines-core/src/observation.rs b/tines-core/src/observation.rs index 873d52df..114cf9df 100644 --- a/tines-core/src/observation.rs +++ b/tines-core/src/observation.rs @@ -46,13 +46,17 @@ pub struct Observation { pub observer: NodeId, /// The observer's per-observation sequence number. pub seq: u64, - /// The observer's elapsed time for this observation, in ms. + /// How long ago the observation was made, in ms: a *lower bound* on its + /// true age. /// - /// This is a *lower bound* on the observation's true age: it counts time - /// only while the observer's clock runs. Time the host spent powered off is - /// recovered when the store is opened (see [`Self::observed_at`]); the time - /// in an un-flushed crash window is bounded by the host's flush cadence, - /// and in-flight time is negligible. Relays never advance it. + /// Every holder advances it by the time the observation spends in its + /// store, so it counts the time spent on running clocks. It misses time a + /// holder was powered off until the holder's store recovers it from a + /// trusted wall clock, time between a holder's last flush and a crash, and + /// time in flight. Every error makes an observation look younger, never + /// older. + /// + /// For absolute time, use [`Self::observed_at`]. pub age_ms: u64, /// The observer's wall-clock time, in ms since the Unix epoch (UTC), when /// this observation was published, or `None` if the observer has no diff --git a/tines-core/src/store.rs b/tines-core/src/store.rs index 2315dfe5..af83d1f5 100644 --- a/tines-core/src/store.rs +++ b/tines-core/src/store.rs @@ -107,14 +107,27 @@ pub trait Store { /// re-inserting them later is accepted. fn trim(&mut self, policy: &TrimPolicy, now: Tick) -> usize; - /// Best-effort durability flush, advancing each stored observation's age - /// up to the caller's `now` and recording `now` as the flush clock. + /// The store's durability point. /// - /// Flush must not lose messages, must never resurrect a released message, - /// and is infallible by contract: a failed flush is, at worst, a - /// duplicate custodian on the next exchange — never a lost message. - /// Backends that cannot persist more cheaply default to a no-op that - /// simply returns `Ok(())`. + /// A flush does two things: + /// - everything inserted, released or trimmed so far becomes durable; + /// - `now` becomes the time anchor for ages after a restart, so a + /// persistent backend can count the time between this flush and the + /// restart into each age. + /// + /// More of the contract: + /// - Flushing twice is harmless. + /// - A `now` earlier than the last anchor is clamped to it. + /// - A flush doesn't change any age [`Self::get`] reports at `now` or + /// later, evict anything, or undo a release. + /// - A failure returns [`StoreError::Backend`] and leaves the store + /// usable. What wasn't made durable is at risk only if the process + /// then stops before a later flush succeeds. + /// - The application decides when to flush; core never calls `flush` + /// itself. On flash, every flush costs wear. + /// + /// The default does nothing and returns `Ok(())`, which is correct for a + /// store that holds nothing across a restart, such as `MemStore`. fn flush(&mut self, now: Tick) -> Result<(), StoreError> { let _ = now; Ok(()) @@ -128,24 +141,38 @@ pub(crate) fn current_age(obs: &Observation, stored_at: Tick, now: Tick) -> u64 obs.age_ms.saturating_add(now.saturating_sub(stored_at)) } -/// A [`Store`] that a host can feed trusted wall-clock time into. +/// A [`Store`] that holds data across power-off and can take trusted wall +/// time from the host (spec §9, decision H). /// -/// tines-core itself never deals with wall time — this trait is a host-facing -/// hook so a host can lift the quarantine on observations held across a -/// power-off, and estimate trusted time from a peer's observation, without a -/// general `&mut Store` backdoor (decision F in the spec). Only backends that -/// support a wall-clock fold implement it; `MemStore` and other pure-tick -/// stores do not. +/// A store that has been powered off can't tell how long it was off from +/// ticks alone, which restart on every boot. A backend that implements this +/// trait holds back what it loaded at start-up (its *quarantine*: not +/// offered, delivered or acked, but still deduped) until the host has +/// trusted wall time, then folds the downtime into those ages. Core never +/// reads or interprets a wall time: this trait only carries the host's value +/// to the backend, and the backend's estimate back to the host. Hosts reach +/// it through [`Node::set_wall_clock`](crate::Node::set_wall_clock) and +/// [`Node::peer_time_estimate`](crate::Node::peer_time_estimate), so no +/// `&mut Store` escapes the facade. Only backends that hold data across +/// power-off implement it; `MemStore` doesn't. pub trait WallClockStore: Store { - /// Summary of a wall-clock fold, for the host's logs. + /// The backend's summary of what a fold did, for the host's logs. type FoldReport; - /// Fold trusted wall time into the store, lifting the quarantine on held - /// observations. The host calls this exactly once, on the first trusted - /// time. + /// Fold trusted wall time into the store: `wall_now` is the host's + /// trusted wall time in ms since the Unix epoch, and `now` the current + /// tick. Lifts the quarantine on what the store held at start-up. + /// + /// The fold runs once, when the host first has trusted time. A second + /// call is a no-op that returns the first fold's report. fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> Self::FoldReport; - /// Best estimate of trusted wall time from a peer's observation stored - /// since the last open, if any. `None` means stay quarantined. + /// An estimate of the current wall time (ms since the Unix epoch) from + /// peers' observations stored since start-up that carry `observed_at`, + /// or `None` if there are none yet. + /// + /// Peer time is unauthenticated until 1.1: a peer can claim any + /// `observed_at`, so the host bounds the estimate before it passes it to + /// [`Self::set_wall_clock`]. fn peer_time_estimate(&self, now: Tick) -> Option; }