113 lines
3.6 KiB
C#
113 lines
3.6 KiB
C#
namespace ZB.MOM.NatsNet.Server.Internal;
|
|
|
|
/// <summary>
|
|
/// Provides an efficiently-cached Unix nanosecond timestamp updated every
|
|
/// <see cref="TickInterval"/> by a shared background timer.
|
|
/// Register before use and Unregister when done; the timer shuts down when all
|
|
/// registrants have unregistered.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Mirrors the Go <c>ats</c> package. Intended for high-frequency cache
|
|
/// access-time reads that do not need sub-100ms precision.
|
|
/// </remarks>
|
|
public static class AccessTimeService
|
|
{
|
|
/// <summary>How often the cached time is refreshed.</summary>
|
|
public static readonly TimeSpan TickInterval = TimeSpan.FromMilliseconds(100);
|
|
|
|
private static long _utime;
|
|
private static long _refs;
|
|
private static Timer? _timer;
|
|
private static readonly object _lock = new();
|
|
|
|
static AccessTimeService()
|
|
{
|
|
// Mirror Go's init(): nothing to pre-allocate in .NET.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Explicit init hook for Go parity.
|
|
/// Mirrors package <c>init()</c> in server/ats/ats.go.
|
|
/// This method is intentionally idempotent.
|
|
/// </summary>
|
|
public static void Init()
|
|
{
|
|
// Ensure a non-zero cached timestamp is present.
|
|
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
|
|
Interlocked.CompareExchange(ref _utime, now, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a user. Starts the background timer when the first registrant calls this.
|
|
/// Each call to <see cref="Register"/> must be paired with a call to <see cref="Unregister"/>.
|
|
/// </summary>
|
|
public static void Register()
|
|
{
|
|
var v = Interlocked.Increment(ref _refs);
|
|
if (v == 1)
|
|
{
|
|
Interlocked.Exchange(ref _utime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L);
|
|
lock (_lock)
|
|
{
|
|
_timer?.Dispose();
|
|
_timer = new Timer(_ =>
|
|
{
|
|
Interlocked.Exchange(ref _utime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L);
|
|
}, null, TickInterval, TickInterval);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a user. Stops the background timer when the last registrant calls this.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Thrown when unregister is called more times than register.</exception>
|
|
public static void Unregister()
|
|
{
|
|
var v = Interlocked.Decrement(ref _refs);
|
|
if (v == 0)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
}
|
|
}
|
|
else if (v < 0)
|
|
{
|
|
Interlocked.Exchange(ref _refs, 0);
|
|
throw new InvalidOperationException("ats: unbalanced unregister for access time state");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the last cached Unix nanosecond timestamp.
|
|
/// If no registrant is active, returns a fresh timestamp (avoids returning zero).
|
|
/// </summary>
|
|
public static long AccessTime()
|
|
{
|
|
var v = Interlocked.Read(ref _utime);
|
|
if (v == 0)
|
|
{
|
|
v = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
|
|
Interlocked.CompareExchange(ref _utime, v, 0);
|
|
v = Interlocked.Read(ref _utime);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all state. For testing only.
|
|
/// </summary>
|
|
internal static void Reset()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
}
|
|
Interlocked.Exchange(ref _refs, 0);
|
|
Interlocked.Exchange(ref _utime, 0);
|
|
}
|
|
}
|