hlc-gen 2.0.0

Lock-free Hybrid Logical Clock (HLC) timestamp generator.
Documentation
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(test)]
#[macro_use]
extern crate std;

mod epoch;
pub mod error;
pub mod source;
mod timestamp;

use core::cmp::Ordering;

use timestamp::HlcAtomicTimestamp;
pub use timestamp::HlcTimestamp;

use crate::error::{HlcError, HlcResult};
use crate::source::{ClockSource, ManualClock, UtcClock};

/// Hybrid Logical Clock (HLC) generator.
pub struct HlcGenerator<S: ClockSource = UtcClock> {
    /// The last timestamp generated by the clock.
    state: HlcAtomicTimestamp,

    /// The maximum drift (in milliseconds) allowed between the physical clock
    /// and the wall-clock time.
    max_drift: usize,

    /// The timestamp provider used to get the current timestamp.
    clock: S,
}

impl Default for HlcGenerator<UtcClock> {
    /// Creates a new HLC clock without any drift.
    fn default() -> Self {
        Self::new(0)
    }
}

impl HlcGenerator<UtcClock> {
    /// Creates a new HLC clock with the specified maximum drift.
    ///
    /// Set `max_drift` to 0, if the clock is running in a single node settings
    /// (useful for generating timestamp-based monotonically increasing
    /// IDs). In such settings, there is no need to worry about drift, as no
    /// adjustments are made to the clock, i.e.
    /// [`update()`](HlcGenerator::update) is never called.
    pub fn new(max_drift: usize) -> Self {
        Self::with_max_drift(max_drift)
    }
}

impl HlcGenerator<ManualClock> {
    /// Creates a new manual HLC clock with the specified maximum drift.
    ///
    /// Useful for testing purposes, where manual timestamps are used.
    pub fn manual(max_drift: usize) -> Self {
        Self::with_max_drift(max_drift)
    }

    pub fn set_current_timestamp(&self, timestamp: i64) {
        self.clock.set_current_timestamp(timestamp);
    }
}

impl<S: ClockSource> HlcGenerator<S> {
    /// Creates a new HLC clock with the specified maximum drift.
    fn with_max_drift(max_drift: usize) -> Self {
        let clock = S::default();
        let state = HlcTimestamp::from_parts(clock.current_timestamp(), 0)
            .unwrap_or_default()
            .into();
        Self {
            state,
            max_drift,
            clock,
        }
    }

    /// Current timestamp.
    ///
    /// Use [`next_timestamp()`](HlcGenerator::next_timestamp) to get the
    /// timestamp for local or send events.
    pub fn timestamp(&self) -> HlcTimestamp {
        self.state.snapshot()
    }

    /// Timestamp for the local or send event.
    pub fn next_timestamp(&self) -> Option<HlcTimestamp> {
        let timestamp = self.clock.current_timestamp();

        self.state
            .update(move |pt, lc| {
                // Update the physical time and increment the logical count.
                if pt >= timestamp {
                    Ok((pt, lc + 1))
                } else {
                    Ok((timestamp, 0))
                }
            })
            .map(|ts| ts.snapshot())
            .ok()
    }

    /// Adjust the clock based on incoming timestamp.
    ///
    /// Usually this happens when a timestamp is received from another node.
    /// An error may occur if drift is exceeded (if `max_drift` is set to 0,
    /// then such a check is ignored).
    ///
    /// Updated timestamp is returned.
    pub fn update(&self, incoming_state: &HlcTimestamp) -> HlcResult<HlcTimestamp> {
        let max_drift = self.max_drift;
        let timestamp = self.clock.current_timestamp();

        self.state
            .update(move |pt, lc| {
                let (incoming_pt, incoming_lc) = incoming_state.parts();

                // Physical clock is ahead of both the incoming timestamp and the current state.
                if timestamp > incoming_pt && timestamp > pt {
                    // Update the clock state.
                    return Ok((timestamp, 0));
                }

                match incoming_pt.cmp(&pt) {
                    // Incoming timestamp is ahead of the current state.
                    Ordering::Greater => {
                        // Check for drift.
                        if max_drift > 0 {
                            let drift = usize::try_from(incoming_pt - timestamp)
                                .map_err(|_| HlcError::OutOfRangeTimestamp)?;
                            if drift > max_drift {
                                return Err(HlcError::DriftTooLarge(drift, max_drift));
                            }
                        }
                        // Remote timestamp is ahead of the current state. Update local state.
                        Ok((incoming_pt, incoming_lc + 1))
                    }
                    // Incoming timestamp is behind the current state.
                    Ordering::Less => {
                        // Our timestamp is ahead of the incoming timestamp, so it remains
                        // unchanged. We only need to update the logical
                        // count.
                        Ok((pt, lc + 1))
                    }
                    // Timestamps are equal, so we need to use the maximum logical count for update.
                    Ordering::Equal => {
                        // Timestamps are equal, so we need to use the maximum logical count for
                        // update.
                        Ok((pt, lc.max(incoming_lc) + 1))
                    }
                }
            })
            .map(|ts| ts.snapshot())
    }
}