tidaldb/tidal/docs/planning/milestone-8/phase-3/task-01-hlc.md
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

4.7 KiB

Task 01: Hybrid Logical Clock (HLC)

Delivers

HLC (Hybrid Logical Clock) in tidal/src/replication/crdt/hlc.rs. Provides now(), update(remote), monotonic guarantee, and PartialOrd/Ord by (wall_ns, logical, node_id). Used by LWWRegister for causal ordering of concurrent writes across nodes.

Complexity: S

Dependencies

  • Phase 8.1 (ShardId used as node_id)

Technical Design

HLC (Kulkarni et al., 2014) combines a wall clock with a logical counter:

  • On send: pt = max(wall, clock.wall); l = if pt == clock.wall { clock.logical + 1 } else { 0 }; clock = (pt, l)
  • On receive(msg_hlc): pt = max(wall, msg_hlc.wall, clock.wall); l = if pt == clock.wall && pt == msg_hlc.wall { max(clock.logical, msg_hlc.logical) + 1 } else if pt == clock.wall { clock.logical + 1 } else if pt == msg_hlc.wall { msg_hlc.logical + 1 } else { 0 }; clock = (pt, l)
// tidal/src/replication/crdt/hlc.rs

/// Hybrid Logical Clock timestamp.
///
/// Combines wall-clock time (ns) with a logical counter to provide
/// causal ordering even with clock skew between nodes.
///
/// Ordering: (wall_ns, logical, node_id) -- lexicographic.
/// This means: same-wall-time events are ordered by logical counter;
/// ties within one node (impossible) are broken by node_id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HlcTimestamp {
    pub wall_ns: u64,
    pub logical: u32,
    pub node_id: u16,  // ShardId::0 for single-node
}

impl PartialOrd for HlcTimestamp {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for HlcTimestamp {
    fn cmp(&self, other: &Self) -> Ordering {
        self.wall_ns.cmp(&other.wall_ns)
            .then(self.logical.cmp(&other.logical))
            .then(self.node_id.cmp(&other.node_id))
    }
}

/// A per-node HLC clock.
pub struct Hlc {
    node_id: u16,
    wall_ns: AtomicU64,
    logical: AtomicU32,
}

impl Hlc {
    pub fn new(node_id: u16) -> Self {
        Self {
            node_id,
            wall_ns: AtomicU64::new(0),
            logical: AtomicU32::new(0),
        }
    }

    fn wall_now() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64
    }

    /// Generate a new HLC timestamp for a local event.
    pub fn now(&self) -> HlcTimestamp {
        let wall = Self::wall_now();
        // Atomic CAS loop to advance monotonically
        loop {
            let cur_wall = self.wall_ns.load(Ordering::Acquire);
            let cur_logical = self.logical.load(Ordering::Acquire);
            let (new_wall, new_logical) = if wall > cur_wall {
                (wall, 0u32)
            } else {
                (cur_wall, cur_logical + 1)
            };
            if self.wall_ns.compare_exchange(cur_wall, new_wall, Ordering::AcqRel, Ordering::Acquire).is_ok() {
                self.logical.store(new_logical, Ordering::Release);
                return HlcTimestamp { wall_ns: new_wall, logical: new_logical, node_id: self.node_id };
            }
        }
    }

    /// Update the clock on receiving a remote HLC timestamp.
    pub fn update(&self, remote: HlcTimestamp) -> HlcTimestamp {
        let wall = Self::wall_now();
        let pt = wall.max(remote.wall_ns);
        loop {
            let cur_wall = self.wall_ns.load(Ordering::Acquire);
            let cur_logical = self.logical.load(Ordering::Acquire);
            let pt = pt.max(cur_wall);
            let new_logical = if pt == cur_wall && pt == remote.wall_ns {
                cur_logical.max(remote.logical) + 1
            } else if pt == cur_wall {
                cur_logical + 1
            } else if pt == remote.wall_ns {
                remote.logical + 1
            } else {
                0
            };
            if self.wall_ns.compare_exchange(cur_wall, pt, Ordering::AcqRel, Ordering::Acquire).is_ok() {
                self.logical.store(new_logical, Ordering::Release);
                return HlcTimestamp { wall_ns: pt, logical: new_logical, node_id: self.node_id };
            }
        }
    }
}

Acceptance Criteria

  • HlcTimestamp ordering is (wall_ns, logical, node_id) lexicographic
  • Hlc::now() returns monotonically increasing timestamps within a single node (property test: 10K calls in sequence never decrease)
  • Hlc::update(remote) advances the clock if remote.wall_ns > current wall
  • Hlc is thread-safe (Send + Sync); concurrent now() calls from 4 threads produce unique timestamps
  • HlcTimestamp derives Serialize, Deserialize, Copy, Clone, PartialEq, Eq
  • cargo clippy -D warnings and cargo fmt pass