feat(localdb): hybrid logical clock (UTC-ms<<16 | counter)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
namespace ZB.MOM.WW.LocalDb.Hlc;
|
||||
|
||||
/// <summary>64-bit HLC: (UTC unix-ms << 16) | 16-bit logical counter. Thread-safe.</summary>
|
||||
public sealed class HybridLogicalClock
|
||||
{
|
||||
private readonly Func<long> _utcNowMs;
|
||||
private readonly Lock _lock = new();
|
||||
private long _last;
|
||||
|
||||
public HybridLogicalClock(long initialValue = 0, Func<long>? utcNowMs = null)
|
||||
{
|
||||
_utcNowMs = utcNowMs ?? (() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
_last = initialValue;
|
||||
}
|
||||
|
||||
public long Next()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var candidate = _utcNowMs() << 16;
|
||||
_last = candidate > _last ? candidate : _last + 1; // +1 rolls counter; overflow naturally carries into physical bits
|
||||
return _last;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Merge a peer's HLC so our next stamp orders after everything we've seen.</summary>
|
||||
public void Observe(long remote) { lock (_lock) { if (remote > _last) _last = remote; } }
|
||||
|
||||
public long Current { get { lock (_lock) return _last; } }
|
||||
|
||||
public static long PhysicalMs(long hlc) => hlc >> 16;
|
||||
public static int Counter(long hlc) => (int)(hlc & 0xFFFF);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using ZB.MOM.WW.LocalDb.Hlc;
|
||||
|
||||
namespace ZB.MOM.WW.LocalDb.Tests;
|
||||
|
||||
public sealed class HybridLogicalClockTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_IsStrictlyMonotonic()
|
||||
{
|
||||
var clock = new HybridLogicalClock();
|
||||
var prev = clock.Next();
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var next = clock.Next();
|
||||
Assert.True(next > prev, $"call {i}: {next} !> {prev}");
|
||||
prev = next;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_UsesUtcPhysicalMs()
|
||||
{
|
||||
const long fixedMs = 1_700_000_000_123;
|
||||
var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);
|
||||
|
||||
Assert.Equal(fixedMs, HybridLogicalClock.PhysicalMs(clock.Next()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_SameMs_IncrementsCounter()
|
||||
{
|
||||
const long fixedMs = 1_700_000_000_000;
|
||||
var clock = new HybridLogicalClock(utcNowMs: () => fixedMs);
|
||||
|
||||
var first = clock.Next();
|
||||
var second = clock.Next();
|
||||
var third = clock.Next();
|
||||
|
||||
Assert.Equal(0, HybridLogicalClock.Counter(first));
|
||||
Assert.Equal(1, HybridLogicalClock.Counter(second));
|
||||
Assert.Equal(2, HybridLogicalClock.Counter(third));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Observe_RemoteAhead_AdvancesPastRemote()
|
||||
{
|
||||
// Frozen wall clock well behind the remote stamp.
|
||||
var clock = new HybridLogicalClock(utcNowMs: () => 1_000);
|
||||
var remote = 5_000_000_000L << 16;
|
||||
|
||||
clock.Observe(remote);
|
||||
|
||||
Assert.True(clock.Next() > remote);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Observe_RemoteBehind_NoRegression()
|
||||
{
|
||||
var clock = new HybridLogicalClock(utcNowMs: () => 1_700_000_000_000);
|
||||
var current = clock.Next();
|
||||
|
||||
clock.Observe(current - 1_000_000); // a stamp from the past
|
||||
|
||||
Assert.Equal(current, clock.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resume_FromPersistedValue_NeverRegresses()
|
||||
{
|
||||
// Persisted value is far ahead of what the (injected) wall clock yields.
|
||||
var persisted = (2_000_000_000_000L << 16) | 0x00FF;
|
||||
var clock = new HybridLogicalClock(initialValue: persisted, utcNowMs: () => 1_000);
|
||||
|
||||
Assert.True(clock.Next() > persisted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CounterOverflow_RollsPhysicalForward()
|
||||
{
|
||||
const long ms = 1_700_000_000_000;
|
||||
// Counter already maxed out under a frozen clock at the same physical ms.
|
||||
var maxed = (ms << 16) | 0xFFFF;
|
||||
var clock = new HybridLogicalClock(initialValue: maxed, utcNowMs: () => ms);
|
||||
|
||||
var next = clock.Next();
|
||||
|
||||
Assert.Equal(ms + 1, HybridLogicalClock.PhysicalMs(next));
|
||||
Assert.Equal(0, HybridLogicalClock.Counter(next));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user