From 297816d6710fb1e3331ddb06ccc121644a296bdb Mon Sep 17 00:00:00 2001 From: iPromKnight Date: Sat, 13 Jul 2024 12:13:21 +0100 Subject: [PATCH] Rate limit RD requests through the http client --- server/RdtClient.Service/DiConfig.cs | 15 +++++- .../Handlers/RateLimitingHandler.cs | 54 +++++++++++++++++++ .../TorrentClients/RealDebridTorrentClient.cs | 2 +- server/RdtClient.Web/Program.cs | 5 +- 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 server/RdtClient.Service/Handlers/RateLimitingHandler.cs diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 75c08e1..c9a1449 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -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(); services.AddScoped(); @@ -30,4 +34,13 @@ public static class DiConfig services.AddHostedService(); services.AddHostedService(); } + + public static void RegisterHttpClients(this IServiceCollection services) + { + services.AddHttpClient(); + + services.AddHttpClient(RD_CLIENT) + .AddHttpMessageHandler(sp => new RateLimitingHandler(sp.GetRequiredService>(), 60, TimeSpan.FromSeconds(60))) + .SetHandlerLifetime(Timeout.InfiniteTimeSpan); + } } \ No newline at end of file diff --git a/server/RdtClient.Service/Handlers/RateLimitingHandler.cs b/server/RdtClient.Service/Handlers/RateLimitingHandler.cs new file mode 100644 index 0000000..89cfd62 --- /dev/null +++ b/server/RdtClient.Service/Handlers/RateLimitingHandler.cs @@ -0,0 +1,54 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace RdtClient.Service.Handlers; + +public class RateLimitingHandler : DelegatingHandler +{ + private readonly ConcurrentQueue _callQueue = new(); + private readonly TimeSpan _requestWindow; + private readonly ILogger _logger; + private readonly Int32 _requestsAllowedPerWindow; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The number of requests allowed per window. + /// The time to delay when the maximum number of requests has been reached (The window). + public RateLimitingHandler(ILogger logger, Int32 requestsAllowedPerWindow, TimeSpan requestWindow) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestsAllowedPerWindow); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(requestWindow, TimeSpan.Zero); + _logger = logger; + _requestsAllowedPerWindow = requestsAllowedPerWindow; + _requestWindow = requestWindow; + } + + /// + protected override async Task 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); + } +} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs index f5955ba..a8ee284 100644 --- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -23,7 +23,7 @@ public class RealDebridTorrentClient(ILogger 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); diff --git a/server/RdtClient.Web/Program.cs b/server/RdtClient.Web/Program.cs index e17fe2c..cceb762 100644 --- a/server/RdtClient.Web/Program.cs +++ b/server/RdtClient.Web/Program.cs @@ -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;