using Microsoft.Extensions.Caching.Memory; namespace StellaOps.Concelier.WebService.Services; internal sealed class MirrorRateLimiter { private readonly IMemoryCache _cache; private readonly TimeProvider _timeProvider; private static readonly TimeSpan Window = TimeSpan.FromHours(1); public MirrorRateLimiter(IMemoryCache cache, TimeProvider timeProvider) { _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } public bool TryAcquire(string domainId, string scope, int limit, out TimeSpan? retryAfter) { retryAfter = null; if (limit <= 0 || limit == int.MaxValue) { return true; } var key = CreateKey(domainId, scope); var now = _timeProvider.GetUtcNow(); var counter = _cache.Get(key); if (counter is null || now - counter.WindowStart >= Window) { counter = new Counter(now, 0); } if (counter.Count >= limit) { var windowEnd = counter.WindowStart + Window; retryAfter = windowEnd > now ? windowEnd - now : TimeSpan.Zero; return false; } counter = counter with { Count = counter.Count + 1 }; var absoluteExpiration = counter.WindowStart + Window; _cache.Set(key, counter, absoluteExpiration); return true; } private static string CreateKey(string domainId, string scope) => string.Create(domainId.Length + scope.Length + 1, (domainId, scope), static (span, state) => { state.domainId.AsSpan().CopyTo(span); span[state.domainId.Length] = '|'; state.scope.AsSpan().CopyTo(span[(state.domainId.Length + 1)..]); }); private sealed record Counter(DateTimeOffset WindowStart, int Count); }