179 lines
6.7 KiB
Rust
179 lines
6.7 KiB
Rust
//! Operation stamps and the hybrid logical clock (Chapter 6 §"Operation
|
|
//! Identity and Stamps").
|
|
//!
|
|
//! An operation carries two related but distinct things: its *identity*
|
|
//! ([`epiphany_core::OperationId`] — who authored it, with what counter) and
|
|
//! its *stamp* (when it was committed, for ordering). Identity is fixed at
|
|
//! authoring and never moves; the stamp is consumed only as ordering metadata
|
|
//! and "never for identity."
|
|
//!
|
|
//! The [`HybridLogicalClock`] combines a physical wall-clock component with a
|
|
//! logical counter that advances when physical time does not. Two derived
|
|
//! orderings come off the stamp:
|
|
//!
|
|
//! * The **per-replica monotonicity tuple** `(physical, logical, id.counter)`
|
|
//! (Chapter 6 §6.6): for two envelopes from one replica with counters
|
|
//! `c1 < c2`, the `c1` tuple MUST be ≤ the `c2` tuple, or the replica stream
|
|
//! is anomalous.
|
|
//! * The **canonical reduction tuple** `(physical, logical, replica, counter)`
|
|
//! (Chapter 6 §6.3.3): the order in which *concurrent* operations reduce.
|
|
//!
|
|
//! Acceptance never trusts stamp *content*: a peer may emit implausible future
|
|
//! times or extreme logical counters, and the reduction consumes the stamp as
|
|
//! ordering metadata without validating plausibility (Chapter 6 §6.4). The one
|
|
//! thing a well-formed stamp MUST satisfy is a finite, non-negative physical
|
|
//! time (checked in [`crate::well_formed`]).
|
|
|
|
use epiphany_core::{OperationId, ReplicaId, WallClockTime};
|
|
use epiphany_determinism::CanonicalEncode;
|
|
|
|
use crate::encode::{push_canon, push_u32};
|
|
|
|
/// A hybrid logical clock: a physical wall-clock component plus a logical
|
|
/// counter advanced when physical time does not move (Chapter 6
|
|
/// §"Operation Identity and Stamps").
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
|
|
pub struct HybridLogicalClock {
|
|
/// Physical time component, in canonical nanosecond units
|
|
/// ([`WallClockTime`]). Well-formed envelopes carry a finite, non-negative
|
|
/// value (Chapter 6 §6.4).
|
|
pub physical_time: WallClockTime,
|
|
/// Logical counter, advanced when physical time does not.
|
|
pub logical_counter: u32,
|
|
}
|
|
|
|
impl HybridLogicalClock {
|
|
/// Builds a clock reading.
|
|
#[inline]
|
|
pub const fn new(physical_time: WallClockTime, logical_counter: u32) -> Self {
|
|
HybridLogicalClock {
|
|
physical_time,
|
|
logical_counter,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for HybridLogicalClock {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
push_canon(out, &self.physical_time);
|
|
push_u32(out, self.logical_counter);
|
|
}
|
|
}
|
|
|
|
/// The ordering stamp of an operation (Chapter 6): a clock reading plus the
|
|
/// operation's identity. The `id` here MUST equal the envelope's top-level
|
|
/// `id` (the `stamp.id == id` well-formedness invariant, Chapter 6 §6.4).
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
|
|
pub struct OperationStamp {
|
|
/// The hybrid logical clock reading at commit.
|
|
pub hlc: HybridLogicalClock,
|
|
/// The operation this stamp addresses. Ordering metadata, never identity.
|
|
pub id: OperationId,
|
|
}
|
|
|
|
impl OperationStamp {
|
|
/// Builds a stamp.
|
|
#[inline]
|
|
pub const fn new(hlc: HybridLogicalClock, id: OperationId) -> Self {
|
|
OperationStamp { hlc, id }
|
|
}
|
|
|
|
/// The canonical reduction tuple `(physical, logical, replica, counter)`
|
|
/// used to order *concurrent* operations (Chapter 6 §6.3.3). Causal order
|
|
/// dominates this, but the authoring HLC rule guarantees a causal
|
|
/// predecessor's tuple is strictly less, so a plain lexicographic sort by
|
|
/// this tuple is already causal-respecting (see [`crate::canonical_reduction_order`]).
|
|
#[inline]
|
|
pub fn reduction_tuple(&self) -> StampTuple {
|
|
StampTuple {
|
|
physical_time: self.hlc.physical_time,
|
|
logical_counter: self.hlc.logical_counter,
|
|
replica: self.id.replica,
|
|
counter: self.id.counter,
|
|
}
|
|
}
|
|
|
|
/// The per-replica monotonicity tuple `(physical, logical, counter)`
|
|
/// (Chapter 6 §6.6). Compared only between two envelopes of the *same*
|
|
/// replica, so the replica field is dropped.
|
|
#[inline]
|
|
pub fn monotonicity_tuple(&self) -> (WallClockTime, u32, u64) {
|
|
(
|
|
self.hlc.physical_time,
|
|
self.hlc.logical_counter,
|
|
self.id.counter,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl CanonicalEncode for OperationStamp {
|
|
#[inline]
|
|
fn encode_canonical(&self, out: &mut Vec<u8>) {
|
|
push_canon(out, &self.hlc);
|
|
push_canon(out, &self.id);
|
|
}
|
|
}
|
|
|
|
/// The canonical reduction tuple of a stamp: `(physical_time, logical_counter,
|
|
/// replica, counter)`, ordered lexicographically ascending (Chapter 6 §6.3.3).
|
|
///
|
|
/// The field declaration order *is* the comparison order, so the derived `Ord`
|
|
/// is exactly the spec's lexicographic order. [`ReplicaId`]'s numeric `Ord`
|
|
/// equals the big-endian byte order the spec names ("lexicographic on the
|
|
/// replica identifier's canonical byte form"), because the replica is the high
|
|
/// bits of an unsigned integer.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct StampTuple {
|
|
/// Physical time, ascending.
|
|
pub physical_time: WallClockTime,
|
|
/// Logical counter, ascending.
|
|
pub logical_counter: u32,
|
|
/// Authoring replica, ascending (= big-endian byte order).
|
|
pub replica: ReplicaId,
|
|
/// Authoring counter, ascending.
|
|
pub counter: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn stamp(p: i64, l: u32, r: u64, c: u64) -> OperationStamp {
|
|
OperationStamp::new(
|
|
HybridLogicalClock::new(WallClockTime(p), l),
|
|
OperationId::new(ReplicaId(r), c),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn reduction_tuple_is_lexicographic_physical_logical_replica_counter() {
|
|
// physical dominates everything
|
|
assert!(stamp(1, 9, 9, 9).reduction_tuple() < stamp(2, 0, 0, 0).reduction_tuple());
|
|
// then logical
|
|
assert!(stamp(5, 1, 9, 9).reduction_tuple() < stamp(5, 2, 0, 0).reduction_tuple());
|
|
// then replica
|
|
assert!(stamp(5, 5, 1, 9).reduction_tuple() < stamp(5, 5, 2, 0).reduction_tuple());
|
|
// then counter
|
|
assert!(stamp(5, 5, 5, 1).reduction_tuple() < stamp(5, 5, 5, 2).reduction_tuple());
|
|
}
|
|
|
|
#[test]
|
|
fn monotonicity_tuple_drops_the_replica() {
|
|
let s = stamp(7, 3, 42, 11);
|
|
assert_eq!(s.monotonicity_tuple(), (WallClockTime(7), 3, 11));
|
|
}
|
|
|
|
#[test]
|
|
fn stamp_encode_is_stable() {
|
|
let s = stamp(123, 4, 5, 6);
|
|
let mut a = Vec::new();
|
|
s.encode_canonical(&mut a);
|
|
let mut b = Vec::new();
|
|
s.encode_canonical(&mut b);
|
|
assert_eq!(a, b);
|
|
// physical(8) + logical(4) + id(16) = 28 bytes.
|
|
assert_eq!(a.len(), 28);
|
|
}
|
|
}
|