Merge pull request #537 from iPromKnight/feature/rd_rate_limit

Rate limit RD requests through the http client
This commit is contained in:
Roger Far 2024-07-13 19:40:40 -06:00 committed by GitHub
commit 617ae547cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 4 deletions

View file

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Handlers;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
using RdtClient.Service.Services.TorrentClients;
@ -9,7 +11,9 @@ namespace RdtClient.Service;
public static class DiConfig
{
public static void Config(IServiceCollection services)
public const String RD_CLIENT = "RdClient";
public static void RegisterRdtServices(this IServiceCollection services)
{
services.AddScoped<AllDebridTorrentClient>();
services.AddScoped<Authentication>();
@ -30,4 +34,13 @@ public static class DiConfig
services.AddHostedService<UpdateChecker>();
services.AddHostedService<WatchFolderChecker>();
}
public static void RegisterHttpClients(this IServiceCollection services)
{
services.AddHttpClient();
services.AddHttpClient(RD_CLIENT)
.AddHttpMessageHandler(sp => new RateLimitingHandler(sp.GetRequiredService<ILogger<RateLimitingHandler>>(), 60, TimeSpan.FromSeconds(60)))
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
}
}

View file

@ -0,0 +1,54 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
namespace RdtClient.Service.Handlers;
public class RateLimitingHandler : DelegatingHandler
{
private readonly ConcurrentQueue<DateTimeOffset> _callQueue = new();
private readonly TimeSpan _requestWindow;
private readonly ILogger<RateLimitingHandler> _logger;
private readonly Int32 _requestsAllowedPerWindow;
/// <summary>
/// Initializes a new instance of the <see cref="RateLimitingHandler"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="requestsAllowedPerWindow">The number of requests allowed per window.</param>
/// <param name="requestWindow">The time to delay when the maximum number of requests has been reached (The window).</param>
public RateLimitingHandler(ILogger<RateLimitingHandler> logger, Int32 requestsAllowedPerWindow, TimeSpan requestWindow)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestsAllowedPerWindow);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(requestWindow, TimeSpan.Zero);
_logger = logger;
_requestsAllowedPerWindow = requestsAllowedPerWindow;
_requestWindow = requestWindow;
}
/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
_callQueue.Enqueue(now);
while (_callQueue.TryPeek(out var oldCall) && now - oldCall > _requestWindow)
{
_logger.LogDebug("Removing oldest request from queue");
_callQueue.TryDequeue(out _);
}
if (_callQueue.Count > _requestsAllowedPerWindow)
{
_logger.LogDebug("Delaying request");
var earliestAllowedRequestTime = now.Add(-_requestWindow);
var delayDuration = _callQueue.ElementAtOrDefault(_requestsAllowedPerWindow - 1) -
earliestAllowedRequestTime;
if (delayDuration > TimeSpan.Zero)
{
_logger.LogDebug("Delaying request for {DelayDuration}", delayDuration);
await Task.Delay(delayDuration, cancellationToken);
}
}
_logger.LogDebug("Sending request");
return await base.SendAsync(request, cancellationToken);
}
}

View file

@ -23,7 +23,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5);

View file

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Hosting.WindowsServices;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
using Serilog;
@ -116,7 +117,7 @@ builder.Services.AddCors(options =>
builder.Services.AddResponseCaching();
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddHttpClient();
builder.Services.RegisterHttpClients();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSession();
@ -133,7 +134,7 @@ builder.Services.AddSignalR(hubOptions =>
builder.Host.UseWindowsService();
RdtClient.Data.DiConfig.Config(builder.Services, appSettings);
RdtClient.Service.DiConfig.Config(builder.Services);
builder.Services.RegisterRdtServices();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;