From f06a89b42b95efdedfd2d7af6f821dc59c223296 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Wed, 30 Oct 2024 16:58:43 +0100 Subject: [PATCH] fix: non-blocking rate-limit for anidb commands Switch from using a monitor and thread sleep (both sync) while rate limiting to using a semaphore slim and thread delay (both async) to not block the thread while we wait for the delay to elapse. --- .../Providers/AniDB/AniDBRateLimiter.cs | 18 ++++++++---------- .../AniDB/HTTP/AniDBHttpConnectionHandler.cs | 8 +++++--- .../AniDB/UDP/AniDBUDPConnectionHandler.cs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs b/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs index 2cb103cbe..870825fa9 100644 --- a/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs +++ b/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Shoko.Server.Providers.AniDB; @@ -8,7 +9,7 @@ namespace Shoko.Server.Providers.AniDB; public abstract class AniDBRateLimiter { private readonly ILogger _logger; - private readonly object _lock = new(); + private readonly SemaphoreSlim _lock = new(1, 1); private readonly Stopwatch _requestWatch = new(); private readonly Stopwatch _activeTimeWatch = new(); @@ -42,14 +43,11 @@ private void ResetRate() _logger.LogTrace("Rate is reset. Active time was {Time} ms", elapsedTime); } - public T EnsureRate(Func action) + public async Task EnsureRateAsync(Func> action) { + await _lock.WaitAsync(); try { - var entered = false; - Monitor.Enter(_lock, ref entered); - if (!entered) throw new SynchronizationLockException(); - var delay = _requestWatch.ElapsedMilliseconds; if (delay > ResetPeriod) ResetRate(); var currentDelay = _activeTimeWatch.ElapsedMilliseconds > ShortPeriod ? LongDelay : ShortDelay; @@ -58,22 +56,22 @@ public T EnsureRate(Func action) { _logger.LogTrace("Time since last request is {Delay} ms, not throttling", delay); _logger.LogTrace("Sending AniDB command"); - return action(); + return await action(); } // add 50ms for good measure var waitTime = currentDelay - (int)delay + 50; _logger.LogTrace("Time since last request is {Delay} ms, throttling for {Time}", delay, waitTime); - Thread.Sleep(waitTime); + await Task.Delay(waitTime); _logger.LogTrace("Sending AniDB command"); - return action(); + return await action(); } finally { _requestWatch.Restart(); - Monitor.Exit(_lock); + _lock.Release(); } } } diff --git a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs index 8c8036063..3c22cf2a6 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs @@ -48,11 +48,12 @@ public async Task> GetHttpDirectly(string url) { throw new AniDBBannedException { - BanType = UpdateType.HTTPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) + BanType = UpdateType.HTTPBan, + BanExpires = BanTime?.AddHours(BanTimerResetLength), }; } - var response = await RateLimiter.EnsureRate(async () => + var response = await RateLimiter.EnsureRateAsync(async () => { using var response = await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -63,7 +64,8 @@ public async Task> GetHttpDirectly(string url) { throw new AniDBBannedException { - BanType = UpdateType.HTTPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) + BanType = UpdateType.HTTPBan, + BanExpires = BanTime?.AddHours(BanTimerResetLength), }; } diff --git a/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs b/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs index f943681b1..6b68b6183 100644 --- a/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs @@ -280,7 +280,7 @@ private async Task SendInternal(string command, bool needsUnicode = true Logger.LogWarning("AniDB request timed out. Checking Network and trying again"); await _connectivityService.CheckAvailability(); }); - var result = await timeoutPolicy.ExecuteAndCaptureAsync(async () => await RateLimiter.EnsureRate(async () => + var result = await timeoutPolicy.ExecuteAndCaptureAsync(async () => await RateLimiter.EnsureRateAsync(async () => { if (_connectivityService.NetworkAvailability < NetworkAvailability.PartialInternet) {