diff --git a/src/Distributed/VectorClock.cs b/src/Distributed/VectorClock.cs index fda7748..1e895c8 100644 --- a/src/Distributed/VectorClock.cs +++ b/src/Distributed/VectorClock.cs @@ -384,7 +384,7 @@ public override string ToString() /// public static VectorClock Parse(string s) { - if (string.IsNullOrEmpty(s)) + if (string.IsNullOrWhiteSpace(s)) return new VectorClock(); var entries = s.Split(',', StringSplitOptions.RemoveEmptyEntries); @@ -394,7 +394,7 @@ public static VectorClock Parse(string s) var pairs = new List<(ushort nodeId, ulong counter)>(); foreach (var entry in entries) { - var parts = entry.Split(':'); + var parts = entry.Split(':', 2, StringSplitOptions.None); if (parts.Length != 2) throw new FormatException($"Invalid vector clock entry: {entry}"); @@ -428,7 +428,7 @@ private static VectorClock CreateCanonical(List<(ushort nodeId, ulong counter)> counters.Add(pair.counter); } - return new VectorClock(nodeIds.ToArray(), counters.ToArray()); + return new VectorClock([.. nodeIds], [.. counters]); } /// @@ -437,7 +437,7 @@ private static VectorClock CreateCanonical(List<(ushort nodeId, ulong counter)> public static bool TryParse(string? s, out VectorClock result) { result = default; - if (string.IsNullOrEmpty(s)) + if (string.IsNullOrWhiteSpace(s)) { result = new VectorClock(); return true; diff --git a/src/Distributed/VectorClockCoordinator.cs b/src/Distributed/VectorClockCoordinator.cs index a4e2229..fc45719 100644 --- a/src/Distributed/VectorClockCoordinator.cs +++ b/src/Distributed/VectorClockCoordinator.cs @@ -26,7 +26,7 @@ namespace Clockworks.Distributed; public sealed class VectorClockCoordinator { private readonly ushort _nodeId; - private readonly object _lock = new(); + private readonly Lock _lock = new(); private VectorClock _current; /// diff --git a/src/HlcGuidFactory.cs b/src/HlcGuidFactory.cs index d1b1520..caa11a7 100644 --- a/src/HlcGuidFactory.cs +++ b/src/HlcGuidFactory.cs @@ -159,19 +159,18 @@ public void NewGuids(Span destination) // Counter overflow - must advance logical time _logicalTimeMs++; _counter = 0; - - // Check drift bounds - var drift = _logicalTimeMs - physicalTimeMs; - if (drift > _options.MaxDriftMs) - { - if (_options.ThrowOnExcessiveDrift) - { - throw new HlcDriftException(drift, _options.MaxDriftMs); - } - // Otherwise, we accept the drift (for high-throughput scenarios) - } } } + + var drift = _logicalTimeMs - physicalTimeMs; + if (drift > _options.MaxDriftMs) + { + if (_options.ThrowOnExcessiveDrift) + { + throw new HlcDriftException(drift, _options.MaxDriftMs); + } + // Otherwise, we accept the drift (for high-throughput scenarios) + } timestamp = new HlcTimestamp(_logicalTimeMs, _counter, _nodeId); diff --git a/tests/HlcGuidFactoryDriftTests.cs b/tests/HlcGuidFactoryDriftTests.cs new file mode 100644 index 0000000..d1f2310 --- /dev/null +++ b/tests/HlcGuidFactoryDriftTests.cs @@ -0,0 +1,43 @@ +using Xunit; + +namespace Clockworks.Tests; + +public sealed class HlcGuidFactoryDriftTests +{ + [Fact] + public void NewGuidWithHlc_WhenClockMovesBackwardBeyondMaxDrift_ThrowsImmediately_WhenStrict() + { + var tp = new SimulatedTimeProvider(); + + using var factory = new HlcGuidFactory( + tp, + nodeId: 1, + options: new HlcOptions { MaxDriftMs = 0, ThrowOnExcessiveDrift = true }); + + _ = factory.NewGuidWithHlc(); + + var now = tp.GetUtcNow(); + tp.SetUtcNow(now - TimeSpan.FromMilliseconds(1)); + + Assert.Throws(() => factory.NewGuidWithHlc()); + } + + [Fact] + public void NewGuidWithHlc_WhenClockMovesBackwardBeyondMaxDrift_DoesNotThrow_WhenNotStrict() + { + var tp = new SimulatedTimeProvider(); + + using var factory = new HlcGuidFactory( + tp, + nodeId: 1, + options: new HlcOptions { MaxDriftMs = 0, ThrowOnExcessiveDrift = false }); + + _ = factory.NewGuidWithHlc(); + + var now = tp.GetUtcNow(); + tp.SetUtcNow(now - TimeSpan.FromMilliseconds(1)); + + var ex = Record.Exception(() => factory.NewGuidWithHlc()); + Assert.Null(ex); + } +}