namespace ZB.MOM.NatsNet.Server.Internal;
///
/// Provides an efficiently-cached Unix nanosecond timestamp updated every
/// by a shared background timer.
/// Register before use and Unregister when done; the timer shuts down when all
/// registrants have unregistered.
///
///
/// Mirrors the Go ats package. Intended for high-frequency cache
/// access-time reads that do not need sub-100ms precision.
///
public static class AccessTimeService
{
/// How often the cached time is refreshed.
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.
}
///
/// Explicit init hook for Go parity.
/// Mirrors package init() in server/ats/ats.go.
/// This method is intentionally idempotent.
///
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);
}
///
/// Registers a user. Starts the background timer when the first registrant calls this.
/// Each call to must be paired with a call to .
///
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);
}
}
}
///
/// Unregisters a user. Stops the background timer when the last registrant calls this.
///
/// Thrown when unregister is called more times than register.
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");
}
}
///
/// Returns the last cached Unix nanosecond timestamp.
/// If no registrant is active, returns a fresh timestamp (avoids returning zero).
///
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;
}
///
/// Resets all state. For testing only.
///
internal static void Reset()
{
lock (_lock)
{
_timer?.Dispose();
_timer = null;
}
Interlocked.Exchange(ref _refs, 0);
Interlocked.Exchange(ref _utime, 0);
}
}