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:
+21
-10
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user