79 lines
2.5 KiB
C#
79 lines
2.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Jellyfin.Plugin.RedditComments.Services;
|
|
|
|
/// <summary>
|
|
/// Sliding-window rate limiter. Guarantees no more than N requests per rolling 60 seconds,
|
|
/// no matter how many callers are waiting.
|
|
/// </summary>
|
|
public class RateLimiter
|
|
{
|
|
private readonly object _lock = new object();
|
|
private readonly Queue<DateTime> _requestTimestamps = new Queue<DateTime>();
|
|
private readonly int? _maxPerMinuteOverride;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="RateLimiter"/> class.
|
|
/// </summary>
|
|
/// <param name="maxPerMinuteOverride">Optional fixed limit (used by tests); otherwise the plugin configuration is used.</param>
|
|
public RateLimiter(int? maxPerMinuteOverride = null)
|
|
{
|
|
_maxPerMinuteOverride = maxPerMinuteOverride;
|
|
}
|
|
|
|
private int MaxPerMinute
|
|
{
|
|
get
|
|
{
|
|
if (_maxPerMinuteOverride.HasValue)
|
|
{
|
|
return _maxPerMinuteOverride.Value;
|
|
}
|
|
|
|
return ReadConfiguredLimit();
|
|
}
|
|
}
|
|
|
|
private static int ReadConfiguredLimit()
|
|
{
|
|
var configured = Plugin.Instance?.Configuration.MaxRequestsPerMinute ?? 60;
|
|
// Reddit's own hard limit for OAuth clients is 100 req/min; never exceed it.
|
|
return Math.Clamp(configured, 1, 100);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits until a request slot is available, then reserves it.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>A task that completes when the caller may perform one request.</returns>
|
|
public async Task WaitAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
while (true)
|
|
{
|
|
TimeSpan delay;
|
|
lock (_lock)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
while (_requestTimestamps.Count > 0 && now - _requestTimestamps.Peek() >= TimeSpan.FromMinutes(1))
|
|
{
|
|
_requestTimestamps.Dequeue();
|
|
}
|
|
|
|
if (_requestTimestamps.Count < MaxPerMinute)
|
|
{
|
|
_requestTimestamps.Enqueue(now);
|
|
return;
|
|
}
|
|
|
|
// Window is full: wait until the oldest request falls out of the window.
|
|
delay = TimeSpan.FromMinutes(1) - (now - _requestTimestamps.Peek()) + TimeSpan.FromMilliseconds(100);
|
|
}
|
|
|
|
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|