Files
Jellyfin-Comments/Jellyfin.Plugin.RedditComments/Services/RateLimiter.cs
Gabrieal Jimmy f321976d70 init
2026-07-16 13:14:58 -05:00

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);
}
}
}