# StellaOps.HybridLogicalClock A Hybrid Logical Clock (HLC) library for deterministic, monotonic job ordering across distributed nodes. HLC combines physical (wall-clock) time with logical counters to provide causally-ordered timestamps even under clock skew. ## Overview ### Problem Statement Distributed systems face challenges with event ordering: - Wall-clock timestamps are susceptible to clock skew between nodes - Logical clocks alone don't provide real-time context - Concurrent events from different nodes need deterministic tie-breaking ### Solution HLC addresses these by combining: - **Physical time** (Unix milliseconds UTC) for real-time context - **Logical counter** for events at the same millisecond - **Node ID** for deterministic tie-breaking across nodes ## Installation Reference the project in your `.csproj`: ```xml ``` ## Quick Start ### Basic Usage ```csharp using StellaOps.HybridLogicalClock; // Create a clock instance var clock = new HybridLogicalClock( TimeProvider.System, nodeId: "scheduler-east-1", stateStore: new InMemoryHlcStateStore(), logger: logger); // Generate timestamps for local events var ts1 = clock.Tick(); // e.g., 1704067200000-scheduler-east-1-000000 var ts2 = clock.Tick(); // e.g., 1704067200000-scheduler-east-1-000001 // Timestamps are always monotonically increasing Debug.Assert(ts2 > ts1); // When receiving a message from another node var remoteTs = HlcTimestamp.Parse("1704067200100-scheduler-west-1-000005"); var mergedTs = clock.Receive(remoteTs); // Merges clocks, returns new timestamp > both ``` ### Dependency Injection ```csharp // Program.cs or Startup.cs // Option 1: In-memory state (development/testing) services.AddHybridLogicalClock( nodeId: Environment.MachineName, maxClockSkew: TimeSpan.FromMinutes(1)); // Option 2: PostgreSQL persistence (production) services.AddHybridLogicalClock( nodeId: Environment.MachineName, maxClockSkew: TimeSpan.FromMinutes(1)); // Option 3: Custom state store factory services.AddHybridLogicalClock( nodeId: Environment.MachineName, stateStoreFactory: sp => new PostgresHlcStateStore( sp.GetRequiredService(), sp.GetRequiredService>()), maxClockSkew: TimeSpan.FromMinutes(1)); ``` Then inject the clock: ```csharp public class JobScheduler(IHybridLogicalClock clock) { public void EnqueueJob(Job job) { job.EnqueuedAt = clock.Tick(); // Jobs are now globally ordered across all scheduler nodes } } ``` ## Core Types ### HlcTimestamp A readonly record struct representing an HLC timestamp: ```csharp public readonly record struct HlcTimestamp : IComparable { public required long PhysicalTime { get; init; } // Unix milliseconds UTC public required string NodeId { get; init; } // e.g., "scheduler-east-1" public required int LogicalCounter { get; init; } // Events at same millisecond } ``` **Key Methods:** | Method | Description | |--------|-------------| | `ToSortableString()` | Returns `"1704067200000-scheduler-east-1-000042"` | | `Parse(string)` | Parses from sortable string format | | `TryParse(string, out HlcTimestamp)` | Safe parsing without exceptions | | `ToDateTimeOffset()` | Converts physical time to DateTimeOffset | | `CompareTo(HlcTimestamp)` | Total ordering comparison | | `IsBefore(HlcTimestamp)` | Returns true if causally before | | `IsAfter(HlcTimestamp)` | Returns true if causally after | | `IsConcurrent(HlcTimestamp)` | True if same time/counter, different nodes | **Comparison Operators:** ```csharp if (ts1 < ts2) { /* ts1 happened before ts2 */ } if (ts1 > ts2) { /* ts1 happened after ts2 */ } if (ts1 <= ts2) { /* ts1 happened at or before ts2 */ } if (ts1 >= ts2) { /* ts1 happened at or after ts2 */ } ``` ### IHybridLogicalClock The main clock interface: ```csharp public interface IHybridLogicalClock { HlcTimestamp Tick(); // Generate timestamp for local event HlcTimestamp Receive(HlcTimestamp remote); // Merge with remote timestamp HlcTimestamp Current { get; } // Current clock state string NodeId { get; } // This node's identifier } ``` ### IHlcStateStore Persistence interface for clock state: ```csharp public interface IHlcStateStore { Task LoadAsync(string nodeId, CancellationToken ct = default); Task SaveAsync(HlcTimestamp timestamp, CancellationToken ct = default); } ``` **Implementations:** - `InMemoryHlcStateStore` - For development and testing - `PostgresHlcStateStore` - For production with durable persistence ## Persistence ### PostgreSQL Schema ```sql CREATE TABLE scheduler.hlc_state ( node_id TEXT PRIMARY KEY, physical_time BIGINT NOT NULL, logical_counter INT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_hlc_state_updated ON scheduler.hlc_state(updated_at DESC); ``` ### PostgresHlcStateStore Uses atomic upsert with conditional update to maintain monotonicity: ```csharp var stateStore = new PostgresHlcStateStore( dataSource, logger, schemaName: "scheduler", // default tableName: "hlc_state" // default ); ``` ## Serialization ### JSON (System.Text.Json) Two converters are provided: 1. **String format** (default) - Compact, sortable: ```json "1704067200000-scheduler-east-1-000042" ``` 2. **Object format** - Explicit properties: ```json { "physicalTime": 1704067200000, "nodeId": "scheduler-east-1", "logicalCounter": 42 } ``` Register converters: ```csharp var options = new JsonSerializerOptions(); options.Converters.Add(new HlcTimestampJsonConverter()); // String format // or options.Converters.Add(new HlcTimestampObjectJsonConverter()); // Object format ``` ### Database (Npgsql/Dapper) Extension methods for reading/writing HLC timestamps: ```csharp // NpgsqlCommand extension await using var cmd = dataSource.CreateCommand(); cmd.CommandText = "INSERT INTO events (timestamp) VALUES (@ts)"; cmd.AddHlcTimestamp("ts", timestamp); await cmd.ExecuteNonQueryAsync(); // NpgsqlDataReader extension await using var reader = await cmd.ExecuteReaderAsync(); var ts = reader.GetHlcTimestamp("timestamp"); var nullableTs = reader.GetHlcTimestampOrNull("timestamp"); ``` Dapper type handlers: ```csharp // Register handlers at startup HlcTypeHandlerRegistration.Register(services); // Then use normally with Dapper var results = await connection.QueryAsync( "SELECT * FROM events WHERE timestamp > @since", new { since = sinceTimestamp }); ``` ## Clock Skew Handling The clock detects and rejects excessive clock skew: ```csharp var clock = new HybridLogicalClock( timeProvider, nodeId, stateStore, logger, maxClockSkew: TimeSpan.FromMinutes(1)); // Default: 1 minute try { var merged = clock.Receive(remoteTimestamp); } catch (HlcClockSkewException ex) { // Remote clock differs by more than maxClockSkew logger.LogWarning( "Clock skew detected: {Actual} exceeds threshold {Max}", ex.ActualSkew, ex.MaxAllowedSkew); } ``` ## Recovery from Restart After a node restart, initialize the clock from persisted state: ```csharp var clock = new HybridLogicalClock(timeProvider, nodeId, stateStore, logger); // Load last persisted state bool recovered = await clock.InitializeFromStateAsync(); if (recovered) { logger.LogInformation("Clock recovered from state: {Current}", clock.Current); } // First tick after restart is guaranteed > last persisted tick var ts = clock.Tick(); ``` ## Testing ### FakeTimeProvider For deterministic testing, use a fake time provider: ```csharp public class FakeTimeProvider : TimeProvider { private DateTimeOffset _now = DateTimeOffset.UtcNow; public override DateTimeOffset GetUtcNow() => _now; public void SetUtcNow(DateTimeOffset value) => _now = value; public void Advance(TimeSpan duration) => _now = _now.Add(duration); } [Fact] public void Tick_Advances_Counter() { var timeProvider = new FakeTimeProvider(); var clock = new HybridLogicalClock(timeProvider, "test", new InMemoryHlcStateStore(), logger); var ts1 = clock.Tick(); var ts2 = clock.Tick(); Assert.Equal(0, ts1.LogicalCounter); Assert.Equal(1, ts2.LogicalCounter); } ``` ## Algorithm ### On Local Event (Tick) ``` l' = l l = max(l, physical_clock()) if l == l': c = c + 1 else: c = 0 return (l, node_id, c) ``` ### On Receive ``` 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) ``` ## Performance Benchmarks on typical hardware: | Operation | Throughput | |-----------|------------| | Tick (single-thread) | > 100,000/sec | | Tick (multi-thread) | > 50,000/sec | | Receive | > 50,000/sec | | Parse | > 500,000/sec | | ToSortableString | > 500,000/sec | | CompareTo | > 10,000,000/sec | Memory: `HlcTimestamp` is a value type (struct) with minimal allocation. ## References - [Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases](https://cse.buffalo.edu/tech-reports/2014-04.pdf) - Kulkarni et al. - [Time, Clocks, and the Ordering of Events in a Distributed System](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) - Lamport