Skip to content

Commit 4f36144

Browse files
committed
Improve request rate limiter in multi threaded scenarios
1 parent 7f4e94c commit 4f36144

File tree

3 files changed

+36
-9
lines changed

3 files changed

+36
-9
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Threading.Tasks;
1+
using System;
2+
using System.Threading.Tasks;
23

34
namespace MatthiWare.FinancialModelingPrep.Abstractions.Http
45
{
56
public interface IRequestRateLimiter
67
{
7-
public Task ThrottleAsync();
8+
public Task<(bool wasThrottled, TimeSpan totalDelay)> ThrottleAsync();
89
public void ReleaseThrottle();
910
}
1011
}

FinancialModelingPrepApi/Core/Http/FinancialModelingPrepHttpClient.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using MatthiWare.FinancialModelingPrep.Abstractions.Http;
22
using MatthiWare.FinancialModelingPrep.Model;
33
using MatthiWare.FinancialModelingPrep.Model.Error;
4+
using Microsoft.Extensions.Logging;
45
using System;
56
using System.Collections.Specialized;
67
using System.Net.Http;
@@ -14,15 +15,19 @@ public class FinancialModelingPrepHttpClient
1415
private readonly HttpClient client;
1516
private readonly FinancialModelingPrepOptions options;
1617
private readonly IRequestRateLimiter rateLimiter;
18+
private readonly ILogger<FinancialModelingPrepHttpClient> logger;
1719
private readonly JsonSerializerOptions jsonSerializerOptions;
1820
private const string EmptyArrayResponse = "[ ]";
1921
private const string ErrorMessageResponse = "Error Message";
2022

21-
public FinancialModelingPrepHttpClient(HttpClient client, FinancialModelingPrepOptions options, IRequestRateLimiter rateLimiter)
23+
public FinancialModelingPrepHttpClient(HttpClient client, FinancialModelingPrepOptions options,
24+
IRequestRateLimiter rateLimiter,
25+
ILogger<FinancialModelingPrepHttpClient> logger)
2226
{
2327
this.client = client ?? throw new ArgumentNullException(nameof(client));
2428
this.options = options ?? throw new ArgumentNullException(nameof(options));
2529
this.rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter));
30+
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
2631
this.jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
2732

2833
if (string.IsNullOrWhiteSpace(this.options.ApiKey))
@@ -35,10 +40,15 @@ public async Task<ApiResponse<string>> GetStringAsync(string urlPattern, NameVal
3540
{
3641
try
3742
{
38-
await rateLimiter.ThrottleAsync();
43+
var (wasThrottled, totalDelay) = await rateLimiter.ThrottleAsync();
3944

4045
var response = await CallApiAsync(urlPattern, pathParams, queryString);
4146

47+
if (wasThrottled)
48+
{
49+
logger.LogDebug("FMP API Call was throttled by {throttle} ms", totalDelay.TotalMilliseconds);
50+
}
51+
4252
if (response.HasError)
4353
{
4454
return ApiResponse.FromError<string>(response.Error);

FinancialModelingPrepApi/Core/Http/RequestRateLimiter.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,38 @@ namespace MatthiWare.FinancialModelingPrep.Core.Http
99
public class RequestRateLimiter : IRequestRateLimiter
1010
{
1111
private readonly SemaphoreSlim threadsLimiter;
12-
private readonly RollingWindowThrottler rollingWindowThrottler;
12+
private readonly RollingWindowThrottler maxApiCallsPerMinuteThrottler;
13+
private readonly RollingWindowThrottler maxRequestsPerSecondThrottler;
1314

1415
public RequestRateLimiter(FinancialModelingPrepOptions options)
1516
{
1617
this.threadsLimiter = new SemaphoreSlim(options.MaxRequestPerSecond, options.MaxRequestPerSecond);
17-
this.rollingWindowThrottler = new RollingWindowThrottler(options.MaxAPICallsPerMinute, TimeSpan.FromMinutes(1));
18+
this.maxApiCallsPerMinuteThrottler = new RollingWindowThrottler(options.MaxAPICallsPerMinute, TimeSpan.FromMinutes(1));
19+
this.maxRequestsPerSecondThrottler = new RollingWindowThrottler(options.MaxRequestPerSecond, TimeSpan.FromSeconds(1));
1820
}
1921

20-
public async Task ThrottleAsync()
21-
{
22+
public async Task<(bool wasThrottled, TimeSpan totalDelay)> ThrottleAsync()
23+
{
24+
var totalDelay = TimeSpan.Zero;
25+
var wasThrottled = false;
26+
2227
await threadsLimiter.WaitAsync();
2328

24-
if (rollingWindowThrottler.ShouldThrottle(out var waitTime))
29+
while (maxRequestsPerSecondThrottler.ShouldThrottle(out var waitTime))
2530
{
31+
wasThrottled = true;
32+
totalDelay = totalDelay.Add(TimeSpan.FromMilliseconds(waitTime));
33+
}
34+
35+
while (maxApiCallsPerMinuteThrottler.ShouldThrottle(out var waitTime))
36+
{
37+
wasThrottled = true;
38+
totalDelay = totalDelay.Add(TimeSpan.FromMilliseconds(waitTime));
39+
2640
await Task.Delay((int)waitTime);
2741
}
42+
43+
return (wasThrottled, totalDelay);
2844
}
2945

3046
public void ReleaseThrottle() => threadsLimiter.Release();

0 commit comments

Comments
 (0)