Merge pull request #537 from iPromKnight/feature/rd_rate_limit
Rate limit RD requests through the http client
This commit is contained in:
commit
617ae547cf
4 changed files with 72 additions and 4 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
54
server/RdtClient.Service/Handlers/RateLimitingHandler.cs
Normal file
54
server/RdtClient.Service/Handlers/RateLimitingHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue