save progress
This commit is contained in:
222
src/__Libraries/StellaOps.HybridLogicalClock/HlcTimestamp.cs
Normal file
222
src/__Libraries/StellaOps.HybridLogicalClock/HlcTimestamp.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
// <copyright file="HlcTimestamp.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.HybridLogicalClock;
|
||||
|
||||
/// <summary>
|
||||
/// Hybrid Logical Clock timestamp providing monotonic, causally-ordered time
|
||||
/// across distributed nodes even under clock skew.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// HLC combines the benefits of physical time (human-readable, bounded drift)
|
||||
/// with logical clocks (guaranteed causality, no rollback). The timestamp
|
||||
/// consists of three components:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>PhysicalTime: Unix milliseconds UTC, advances with wall clock</description></item>
|
||||
/// <item><description>NodeId: Unique identifier for the generating node</description></item>
|
||||
/// <item><description>LogicalCounter: Increments when events occur at same physical time</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Total ordering is defined as: (PhysicalTime, LogicalCounter, NodeId)
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(HlcTimestampJsonConverter))]
|
||||
public readonly record struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable
|
||||
{
|
||||
/// <summary>
|
||||
/// Physical time component (Unix milliseconds UTC).
|
||||
/// </summary>
|
||||
public required long PhysicalTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique node identifier (e.g., "scheduler-east-1").
|
||||
/// </summary>
|
||||
public required string NodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Logical counter for events at same physical time.
|
||||
/// </summary>
|
||||
public required int LogicalCounter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the physical time as a <see cref="DateTimeOffset"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset PhysicalDateTime =>
|
||||
DateTimeOffset.FromUnixTimeMilliseconds(PhysicalTime);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a zero/uninitialized timestamp.
|
||||
/// </summary>
|
||||
public static HlcTimestamp Zero => new()
|
||||
{
|
||||
PhysicalTime = 0,
|
||||
NodeId = string.Empty,
|
||||
LogicalCounter = 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// String representation for storage: "0001704067200000-scheduler-east-1-000042".
|
||||
/// Format: {PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}
|
||||
/// </summary>
|
||||
/// <returns>A sortable string representation.</returns>
|
||||
public string ToSortableString()
|
||||
{
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse from sortable string format.
|
||||
/// </summary>
|
||||
/// <param name="value">The sortable string to parse.</param>
|
||||
/// <returns>The parsed <see cref="HlcTimestamp"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when value is null.</exception>
|
||||
/// <exception cref="FormatException">Thrown when value is not in valid format.</exception>
|
||||
public static HlcTimestamp Parse(string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (!TryParse(value, out var result))
|
||||
{
|
||||
throw new FormatException($"Invalid HLC timestamp format: '{value}'");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse from sortable string format.
|
||||
/// </summary>
|
||||
/// <param name="value">The sortable string to parse.</param>
|
||||
/// <param name="result">The parsed timestamp if successful.</param>
|
||||
/// <returns>True if parsing succeeded; otherwise false.</returns>
|
||||
public static bool TryParse(
|
||||
[NotNullWhen(true)] string? value,
|
||||
out HlcTimestamp result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format: {PhysicalTime:D13}-{NodeId}-{LogicalCounter:D6}
|
||||
// Example: 0001704067200000-scheduler-east-1-000042
|
||||
// The NodeId can contain hyphens, so we parse from both ends
|
||||
|
||||
var firstDash = value.IndexOf('-', StringComparison.Ordinal);
|
||||
if (firstDash < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastDash = value.LastIndexOf('-');
|
||||
if (lastDash <= firstDash || lastDash >= value.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var physicalTimeStr = value[..firstDash];
|
||||
var nodeId = value[(firstDash + 1)..lastDash];
|
||||
var counterStr = value[(lastDash + 1)..];
|
||||
|
||||
if (!long.TryParse(physicalTimeStr, NumberStyles.None, CultureInfo.InvariantCulture, out var physicalTime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(counterStr, NumberStyles.None, CultureInfo.InvariantCulture, out var counter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = physicalTime,
|
||||
NodeId = nodeId,
|
||||
LogicalCounter = counter
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare for total ordering.
|
||||
/// Order: (PhysicalTime, LogicalCounter, NodeId).
|
||||
/// </summary>
|
||||
/// <param name="other">The other timestamp to compare.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public int CompareTo(HlcTimestamp other)
|
||||
{
|
||||
// Primary: physical time
|
||||
var physicalCompare = PhysicalTime.CompareTo(other.PhysicalTime);
|
||||
if (physicalCompare != 0)
|
||||
{
|
||||
return physicalCompare;
|
||||
}
|
||||
|
||||
// Secondary: logical counter
|
||||
var counterCompare = LogicalCounter.CompareTo(other.LogicalCounter);
|
||||
if (counterCompare != 0)
|
||||
{
|
||||
return counterCompare;
|
||||
}
|
||||
|
||||
// Tertiary: node ID (for stable tie-breaking)
|
||||
return string.Compare(NodeId, other.NodeId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (obj is HlcTimestamp other)
|
||||
{
|
||||
return CompareTo(other);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Object must be of type {nameof(HlcTimestamp)}", nameof(obj));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Less than operator.
|
||||
/// </summary>
|
||||
public static bool operator <(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) < 0;
|
||||
|
||||
/// <summary>
|
||||
/// Less than or equal operator.
|
||||
/// </summary>
|
||||
public static bool operator <=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) <= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Greater than operator.
|
||||
/// </summary>
|
||||
public static bool operator >(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Greater than or equal operator.
|
||||
/// </summary>
|
||||
public static bool operator >=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) >= 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => ToSortableString();
|
||||
}
|
||||
Reference in New Issue
Block a user