58 lines
1.9 KiB
C#
58 lines
1.9 KiB
C#
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<Counter>(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);
|
|
}
|