docs(core): rewrite flush, WallClockStore and age_ms rustdoc from the spec

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 08:44:09 -07:00
co-authored by Claude Opus 5.5
parent 649b2a8900
commit 01db6c0b71
3 changed files with 78 additions and 36 deletions
+21 -10
View File
@@ -219,8 +219,13 @@ impl<S: Store, T: Transport> Node<S, T> {
&self.store &self.store
} }
/// Lift the quarantine on held observations once the host has trusted wall /// Pass trusted wall time (ms since the Unix epoch) to a
/// time, by folding the wall clock into a [`WallClockStore`] backend. /// [`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) -> <S as WallClockStore>::FoldReport pub fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> <S as WallClockStore>::FoldReport
where where
S: WallClockStore, S: WallClockStore,
@@ -228,8 +233,11 @@ impl<S: Store, T: Transport> Node<S, T> {
self.store.set_wall_clock(wall_now, now) self.store.set_wall_clock(wall_now, now)
} }
/// Best estimate of trusted wall time from a peer's observation, if the /// The backend's estimate of the current wall time from peers'
/// backend supports it (see [`WallClockStore`]). /// 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<u64> pub fn peer_time_estimate(&self, now: Tick) -> Option<u64>
where where
S: WallClockStore, S: WallClockStore,
@@ -237,13 +245,16 @@ impl<S: Store, T: Transport> Node<S, T> {
self.store.peer_time_estimate(now) self.store.peer_time_estimate(now)
} }
/// Best-effort durability flush, advancing each stored observation's age /// Flush the store: its durability point (see [`Store::flush`]).
/// up to `now`.
/// ///
/// This is the durability boundary a host reaches on its flush schedule — /// Everything inserted, released or trimmed so far becomes durable, and
/// delegates to [`Store::flush`], so it is infallible by contract: a failed /// `now` becomes the time anchor for ages after a restart. Flushing twice
/// flush is, at worst, a duplicate custodian on the next exchange, never a /// is harmless, and a `now` earlier than the last anchor is clamped to it.
/// lost message. /// 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> { pub fn flush(&mut self, now: Tick) -> Result<(), StoreError> {
self.store.flush(now) self.store.flush(now)
} }
+10 -6
View File
@@ -46,13 +46,17 @@ pub struct Observation {
pub observer: NodeId, pub observer: NodeId,
/// The observer's per-observation sequence number. /// The observer's per-observation sequence number.
pub seq: u64, 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 /// Every holder advances it by the time the observation spends in its
/// only while the observer's clock runs. Time the host spent powered off is /// store, so it counts the time spent on running clocks. It misses time a
/// recovered when the store is opened (see [`Self::observed_at`]); the time /// holder was powered off until the holder's store recovers it from a
/// in an un-flushed crash window is bounded by the host's flush cadence, /// trusted wall clock, time between a holder's last flush and a crash, and
/// and in-flight time is negligible. Relays never advance it. /// time in flight. Every error makes an observation look younger, never
/// older.
///
/// For absolute time, use [`Self::observed_at`].
pub age_ms: u64, pub age_ms: u64,
/// The observer's wall-clock time, in ms since the Unix epoch (UTC), when /// 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 /// this observation was published, or `None` if the observer has no
+47 -20
View File
@@ -107,14 +107,27 @@ pub trait Store {
/// re-inserting them later is accepted. /// re-inserting them later is accepted.
fn trim(&mut self, policy: &TrimPolicy, now: Tick) -> usize; fn trim(&mut self, policy: &TrimPolicy, now: Tick) -> usize;
/// Best-effort durability flush, advancing each stored observation's age /// The store's durability point.
/// up to the caller's `now` and recording `now` as the flush clock.
/// ///
/// Flush must not lose messages, must never resurrect a released message, /// A flush does two things:
/// and is infallible by contract: a failed flush is, at worst, a /// - everything inserted, released or trimmed so far becomes durable;
/// duplicate custodian on the next exchange — never a lost message. /// - `now` becomes the time anchor for ages after a restart, so a
/// Backends that cannot persist more cheaply default to a no-op that /// persistent backend can count the time between this flush and the
/// simply returns `Ok(())`. /// 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> { fn flush(&mut self, now: Tick) -> Result<(), StoreError> {
let _ = now; let _ = now;
Ok(()) 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)) 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 /// A store that has been powered off can't tell how long it was off from
/// hook so a host can lift the quarantine on observations held across a /// ticks alone, which restart on every boot. A backend that implements this
/// power-off, and estimate trusted time from a peer's observation, without a /// trait holds back what it loaded at start-up (its *quarantine*: not
/// general `&mut Store` backdoor (decision F in the spec). Only backends that /// offered, delivered or acked, but still deduped) until the host has
/// support a wall-clock fold implement it; `MemStore` and other pure-tick /// trusted wall time, then folds the downtime into those ages. Core never
/// stores do not. /// 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 { 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; type FoldReport;
/// Fold trusted wall time into the store, lifting the quarantine on held /// Fold trusted wall time into the store: `wall_now` is the host's
/// observations. The host calls this exactly once, on the first trusted /// trusted wall time in ms since the Unix epoch, and `now` the current
/// time. /// 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; 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 /// An estimate of the current wall time (ms since the Unix epoch) from
/// since the last open, if any. `None` means stay quarantined. /// 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<u64>; fn peer_time_estimate(&self, now: Tick) -> Option<u64>;
} }