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
}
/// 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) -> <S as WallClockStore>::FoldReport
where
S: WallClockStore,
@@ -228,8 +233,11 @@ impl<S: Store, T: Transport> Node<S, T> {
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<u64>
where
S: WallClockStore,
@@ -237,13 +245,16 @@ impl<S: Store, T: Transport> Node<S, T> {
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)
}
+10 -6
View File
@@ -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
+47 -20
View File
@@ -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<u64>;
}