// ----------------------------------------------------------------------------- // HybridLogicalClock.cs // Sprint: SPRINT_20260105_002_001_LB_hlc_core_library // Task: HLC-003 - Implement HybridLogicalClock class with Tick/Receive/Current // ----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; namespace StellaOps.HybridLogicalClock; /// /// Implementation of Hybrid Logical Clock algorithm for deterministic, /// monotonic timestamp generation across distributed nodes. /// /// /// /// The HLC algorithm combines physical (wall-clock) time with a logical counter: /// - Physical time provides approximate real-time ordering /// - Logical counter ensures monotonicity when physical time doesn't advance /// - Node ID provides stable tie-breaking for concurrent events /// /// /// On local event or send: /// /// l' = l /// l = max(l, physical_clock()) /// if l == l': /// c = c + 1 /// else: /// c = 0 /// return (l, node_id, c) /// /// /// /// On receive(m_l, m_c): /// /// l' = l /// l = max(l', m_l, physical_clock()) /// if l == l' == m_l: /// c = max(c, m_c) + 1 /// elif l == l': /// c = c + 1 /// elif l == m_l: /// c = m_c + 1 /// else: /// c = 0 /// return (l, node_id, c) /// /// /// public sealed class HybridLogicalClock : IHybridLogicalClock { private readonly TimeProvider _timeProvider; private readonly string _nodeId; private readonly IHlcStateStore _stateStore; private readonly TimeSpan _maxClockSkew; private readonly ILogger _logger; private long _lastPhysicalTime; private int _logicalCounter; private readonly object _lock = new(); /// public string NodeId => _nodeId; /// public HlcTimestamp Current { get { lock (_lock) { return new HlcTimestamp { PhysicalTime = _lastPhysicalTime, NodeId = _nodeId, LogicalCounter = _logicalCounter }; } } } /// /// Creates a new Hybrid Logical Clock instance. /// /// Time provider for wall-clock time /// Unique identifier for this node (e.g., "scheduler-east-1") /// Persistent storage for clock state /// Logger for diagnostics /// Maximum allowed clock skew (default: 1 minute) public HybridLogicalClock( TimeProvider timeProvider, string nodeId, IHlcStateStore stateStore, ILogger logger, TimeSpan? maxClockSkew = null) { ArgumentNullException.ThrowIfNull(timeProvider); ArgumentException.ThrowIfNullOrWhiteSpace(nodeId); ArgumentNullException.ThrowIfNull(stateStore); ArgumentNullException.ThrowIfNull(logger); _timeProvider = timeProvider; _nodeId = nodeId; _stateStore = stateStore; _logger = logger; _maxClockSkew = maxClockSkew ?? TimeSpan.FromMinutes(1); // Initialize to 0 so first Tick() will advance physical time and reset counter // This follows the standard HLC algorithm where l starts at 0 _lastPhysicalTime = 0; _logicalCounter = 0; _logger.LogInformation( "HLC initialized for node {NodeId} with max skew {MaxSkew}", _nodeId, _maxClockSkew); } /// /// Initialize clock from persisted state (call during startup). /// /// Cancellation token /// True if state was recovered, false if starting fresh public async Task InitializeFromStateAsync(CancellationToken ct = default) { var persistedState = await _stateStore.LoadAsync(_nodeId, ct); if (persistedState.HasValue) { lock (_lock) { // Ensure we start at least at the persisted time var physicalNow = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); _lastPhysicalTime = Math.Max(physicalNow, persistedState.Value.PhysicalTime); // If we're at the same physical time as persisted, increment counter if (_lastPhysicalTime == persistedState.Value.PhysicalTime) { _logicalCounter = persistedState.Value.LogicalCounter + 1; } else { _logicalCounter = 0; } } _logger.LogInformation( "HLC for node {NodeId} recovered from persisted state: {Timestamp}", _nodeId, persistedState.Value); return true; } _logger.LogInformation( "HLC for node {NodeId} starting fresh (no persisted state)", _nodeId); return false; } /// public HlcTimestamp Tick() { HlcTimestamp timestamp; lock (_lock) { var physicalNow = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); if (physicalNow > _lastPhysicalTime) { // Physical time advanced - reset counter _lastPhysicalTime = physicalNow; _logicalCounter = 0; } else { // Physical time hasn't advanced - increment counter _logicalCounter++; // Check for counter overflow (unlikely but handle it) if (_logicalCounter < 0) { _logger.LogWarning( "HLC counter overflow for node {NodeId}, forcing time advance", _nodeId); // Force time advance to next millisecond _lastPhysicalTime++; _logicalCounter = 0; } } timestamp = new HlcTimestamp { PhysicalTime = _lastPhysicalTime, NodeId = _nodeId, LogicalCounter = _logicalCounter }; } // Persist state asynchronously (fire-and-forget with error logging) _ = PersistStateAsync(timestamp); return timestamp; } /// public HlcTimestamp Receive(HlcTimestamp remote) { HlcTimestamp timestamp; lock (_lock) { var physicalNow = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); // Validate clock skew var skew = TimeSpan.FromMilliseconds(Math.Abs(remote.PhysicalTime - physicalNow)); if (skew > _maxClockSkew) { _logger.LogError( "Clock skew of {Skew} from node {RemoteNode} exceeds threshold {MaxSkew}", skew, remote.NodeId, _maxClockSkew); throw new HlcClockSkewException(skew, _maxClockSkew); } // Find maximum physical time var maxPhysical = Math.Max(Math.Max(_lastPhysicalTime, remote.PhysicalTime), physicalNow); // Apply HLC receive algorithm if (maxPhysical == _lastPhysicalTime && maxPhysical == remote.PhysicalTime) { // All three equal - take max counter and increment _logicalCounter = Math.Max(_logicalCounter, remote.LogicalCounter) + 1; } else if (maxPhysical == _lastPhysicalTime) { // Our time is max - just increment our counter _logicalCounter++; } else if (maxPhysical == remote.PhysicalTime) { // Remote time is max - take their counter and increment _logicalCounter = remote.LogicalCounter + 1; } else { // Physical clock is max - reset counter _logicalCounter = 0; } _lastPhysicalTime = maxPhysical; timestamp = new HlcTimestamp { PhysicalTime = _lastPhysicalTime, NodeId = _nodeId, LogicalCounter = _logicalCounter }; } // Persist state asynchronously _ = PersistStateAsync(timestamp); _logger.LogDebug( "HLC receive from {RemoteNode}: {RemoteTimestamp} -> {LocalTimestamp}", remote.NodeId, remote, timestamp); return timestamp; } private Task PersistStateAsync(HlcTimestamp timestamp) { try { var saveTask = _stateStore.SaveAsync(timestamp); if (saveTask.IsCompletedSuccessfully) { return Task.CompletedTask; } return PersistStateAsyncSlow(saveTask, timestamp); } catch (Exception ex) { _logger.LogWarning( ex, "Failed to persist HLC state for node {NodeId}: {Timestamp}", _nodeId, timestamp); return Task.CompletedTask; } } private async Task PersistStateAsyncSlow(Task saveTask, HlcTimestamp timestamp) { try { await saveTask.ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning( ex, "Failed to persist HLC state for node {NodeId}: {Timestamp}", _nodeId, timestamp); } } }