Skip to content

Commit ca2245b

Browse files
committed
Add thread bases and time based rate limiter
- Max amount of requests per second - Max amount of API calls per minute
1 parent 0bd76fb commit ca2245b

6 files changed

Lines changed: 80 additions & 22 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Threading.Tasks;
2+
3+
namespace MatthiWare.FinancialModelingPrep.Abstractions.Http
4+
{
5+
public interface IRequestRateLimiter
6+
{
7+
public Task ThrottleAsync();
8+
public void ReleaseThrottle();
9+
}
10+
}

FinancialModelingPrepApi/Core/Http/FinancialModelingPrepHttpClient.cs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
using MatthiWare.FinancialModelingPrep.Model;
1+
using MatthiWare.FinancialModelingPrep.Abstractions.Http;
2+
using MatthiWare.FinancialModelingPrep.Model;
23
using MatthiWare.FinancialModelingPrep.Model.Error;
34
using System;
45
using System.Collections.Specialized;
56
using System.Net.Http;
67
using System.Text.Json;
7-
using System.Threading;
88
using System.Threading.Tasks;
99

1010
namespace MatthiWare.FinancialModelingPrep.Core.Http
@@ -13,17 +13,17 @@ public class FinancialModelingPrepHttpClient
1313
{
1414
private readonly HttpClient client;
1515
private readonly FinancialModelingPrepOptions options;
16+
private readonly IRequestRateLimiter rateLimiter;
1617
private readonly JsonSerializerOptions jsonSerializerOptions;
1718
private const string EmptyArrayResponse = "[ ]";
1819
private const string ErrorMessageResponse = "Error Message";
19-
private readonly SemaphoreSlim throttler;
2020

21-
public FinancialModelingPrepHttpClient(HttpClient client, FinancialModelingPrepOptions options)
21+
public FinancialModelingPrepHttpClient(HttpClient client, FinancialModelingPrepOptions options, IRequestRateLimiter rateLimiter)
2222
{
2323
this.client = client ?? throw new ArgumentNullException(nameof(client));
2424
this.options = options ?? throw new ArgumentNullException(nameof(options));
25+
this.rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter));
2526
this.jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
26-
this.throttler = new SemaphoreSlim(options.MaxRequestLimit);
2727

2828
if (string.IsNullOrWhiteSpace(this.options.ApiKey))
2929
{
@@ -33,30 +33,35 @@ public FinancialModelingPrepHttpClient(HttpClient client, FinancialModelingPrepO
3333

3434
public async Task<ApiResponse<string>> GetStringAsync(string urlPattern, NameValueCollection pathParams, QueryStringBuilder queryString)
3535
{
36-
await throttler.WaitAsync();
36+
try
37+
{
38+
await rateLimiter.ThrottleAsync();
3739

38-
var response = await CallApiAsync(urlPattern, pathParams, queryString);
40+
var response = await CallApiAsync(urlPattern, pathParams, queryString);
3941

40-
throttler.Release();
42+
if (response.HasError)
43+
{
44+
return ApiResponse.FromError<string>(response.Error);
45+
}
4146

42-
if (response.HasError)
43-
{
44-
return ApiResponse.FromError<string>(response.Error);
45-
}
47+
if (response.Data.Contains(ErrorMessageResponse))
48+
{
49+
var errorData = JsonSerializer.Deserialize<ErrorResponse>(response.Data);
4650

47-
if (response.Data.Contains(ErrorMessageResponse))
48-
{
49-
var errorData = JsonSerializer.Deserialize<ErrorResponse>(response.Data);
51+
return ApiResponse.FromError<string>(errorData.ErrorMessage);
52+
}
5053

51-
return ApiResponse.FromError<string>(errorData.ErrorMessage);
52-
}
54+
if (response.Data.Equals(EmptyArrayResponse, StringComparison.OrdinalIgnoreCase))
55+
{
56+
return ApiResponse.FromError<string>("Invalid parameters");
57+
}
5358

54-
if (response.Data.Equals(EmptyArrayResponse, StringComparison.OrdinalIgnoreCase))
59+
return ApiResponse.FromSucces(response.Data);
60+
}
61+
finally
5562
{
56-
return ApiResponse.FromError<string>("Invalid parameters");
63+
rateLimiter.ReleaseThrottle();
5764
}
58-
59-
return ApiResponse.FromSucces(response.Data);
6065
}
6166

6267
public async Task<ApiResponse<T>> GetJsonAsync<T>(string urlPattern, NameValueCollection pathParams, QueryStringBuilder queryString)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Bert.RateLimiters;
2+
using MatthiWare.FinancialModelingPrep.Abstractions.Http;
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace MatthiWare.FinancialModelingPrep.Core.Http
8+
{
9+
public class RequestRateLimiter : IRequestRateLimiter
10+
{
11+
private readonly SemaphoreSlim threadsLimiter;
12+
private readonly RollingWindowThrottler rollingWindowThrottler;
13+
14+
public RequestRateLimiter(FinancialModelingPrepOptions options)
15+
{
16+
this.threadsLimiter = new SemaphoreSlim(options.MaxRequestPerSecond, options.MaxRequestPerSecond);
17+
this.rollingWindowThrottler = new RollingWindowThrottler(options.MaxAPICallsPerMinute, TimeSpan.FromMinutes(1));
18+
}
19+
20+
public async Task ThrottleAsync()
21+
{
22+
await threadsLimiter.WaitAsync();
23+
24+
if (rollingWindowThrottler.ShouldThrottle(out var waitTime))
25+
{
26+
await Task.Delay((int)waitTime);
27+
}
28+
}
29+
30+
public void ReleaseThrottle() => threadsLimiter.Release();
31+
}
32+
}

FinancialModelingPrepApi/DependencyInjectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using MatthiWare.FinancialModelingPrep.Abstractions.AdvancedData;
22
using MatthiWare.FinancialModelingPrep.Abstractions.Calendars;
33
using MatthiWare.FinancialModelingPrep.Abstractions.CompanyValuation;
4+
using MatthiWare.FinancialModelingPrep.Abstractions.Http;
45
using MatthiWare.FinancialModelingPrep.Abstractions.InstitutionalFund;
56
using MatthiWare.FinancialModelingPrep.Abstractions.MarketIndexes;
67
using MatthiWare.FinancialModelingPrep.Abstractions.StockTimeSeries;
@@ -36,6 +37,7 @@ public static void AddFinancialModelingPrepApiClient(this IServiceCollection ser
3637
=> client.BaseAddress = new Uri("https://financialmodelingprep.com/api/"));
3738

3839
services.TryAddSingleton<IFinancialModelingPrepApiClient, FinancialModelingPrepApiClient>();
40+
services.TryAddSingleton<IRequestRateLimiter, RequestRateLimiter>();
3941
services.TryAddTransient<ICompanyValuationProvider, CompanyValuationProvider>();
4042
services.TryAddTransient<IMarketIndexesProvider, MarketIndexesProvider>();
4143
services.TryAddTransient<IAdvancedDataProvider, AdvancedDataProvider>();

FinancialModelingPrepApi/FinancialModelingPrepApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
</PropertyGroup>
3030

3131
<ItemGroup>
32+
<PackageReference Include="Bert.RateLimiters" Version="1.0.15" />
3233
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
3334
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
3435
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />

FinancialModelingPrepApi/FinancialModelingPrepOptions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ public class FinancialModelingPrepOptions
1717
/// By default the max allowed is 10. If you have a different rate limit you can configure it here.
1818
/// See Rate Limitation #15 https://financialmodelingprep.com/developer/docs/terms-of-service
1919
/// </summary>
20-
public int MaxRequestLimit { get; set; } = 10;
20+
public int MaxRequestPerSecond { get; set; } = 10;
21+
22+
/// <summary>
23+
/// Gets or sets the maximum allowed API Calls per second.
24+
/// You can find the defaults on the pricing documentation.
25+
/// By default we use the 300 "starter" limit.
26+
/// https://financialmodelingprep.com/developer/docs/pricing
27+
/// </summary>
28+
public int MaxAPICallsPerMinute { get; set; } = 300;
2129
}
2230
}

0 commit comments

Comments
 (0)