Skip to content

Commit 714776c

Browse files
committed
2.37.0 Initial - rate limiter config
1 parent fea8ad0 commit 714776c

11 files changed

Lines changed: 345 additions & 16 deletions

File tree

NpgsqlRest/Defaults/DefaultCommentParser.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ internal static class DefaultCommentParser
199199
"retry_strategy",
200200
"retry",
201201
];
202+
203+
private static readonly string[] RateLimiterPolicyKey = [
204+
"rate_limiter_policy_name",
205+
"rate_limiter_policy",
206+
"rate_limiter",
207+
"rate",
208+
];
202209

203210
public static RoutineEndpoint? Parse(
204211
Routine routine,
@@ -1162,6 +1169,16 @@ internal static class DefaultCommentParser
11621169
logger?.RetryStrategyNotFound(description, name);
11631170
}
11641171
}
1172+
1173+
// rate_limiter_policy_name [ name ]
1174+
// rate_limiter_policy [ name ]
1175+
// rate_limiter [ name ]
1176+
// rate [ name ]
1177+
else if (haveTag is true && len >= 2 && StrEqualsToArray(wordsLower[0], RateLimiterPolicyKey))
1178+
{
1179+
routineEndpoint.RateLimiterPolicy = string.Join(Consts.Space, words[1..]);
1180+
logger?.RateLimiterPolicySet(description, routineEndpoint.RateLimiterPolicy);
1181+
}
11651182
}
11661183
if (disabled)
11671184
{
@@ -1315,11 +1332,16 @@ public static void SetCustomParameter(RoutineEndpoint endpoint, string name, str
13151332

13161333
else if (StrEqualsToArray(name, RetryStrategyKey))
13171334
{
1318-
if (options.CommandRetryOptions.Strategies.TryGetValue(name, out var strategy))
1335+
if (options.CommandRetryOptions.Strategies.TryGetValue(value, out var strategy))
13191336
{
13201337
endpoint.RetryStrategy = strategy;
13211338
}
13221339
}
1340+
1341+
else if (StrEqualsToArray(name, RateLimiterPolicyKey))
1342+
{
1343+
endpoint.RateLimiterPolicy = value;
1344+
}
13231345

13241346
else
13251347
{

NpgsqlRest/Log.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,12 @@ public static partial class Log
212212
[LoggerMessage(Level = LogLevel.Warning, Message = "{description} has failed to set BASIC AUTH USER by the comment annotation.")]
213213
public static partial void BasicAuthUserFailed(this ILogger logger, string description);
214214

215-
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set RETRY STRATEGY set to {name} by the comment annotation.")]
215+
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set RETRY STRATEGY to {name} by the comment annotation.")]
216216
public static partial void RetryStrategySet(this ILogger logger, string description, string name);
217217

218218
[LoggerMessage(Level = LogLevel.Warning, Message = "{description} has failed to set RETRY STRATEGY by the comment annotation. Strategy {name} not found in the list of strategies.")]
219219
public static partial void RetryStrategyNotFound(this ILogger logger, string description, string name);
220-
221-
222-
223-
220+
224221
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to open connection on attempt {attempt}. Retrying in {delay}ms. Error: {error}")]
225222
public static partial void FailedToOpenConnectionRetry(this ILogger logger, int attempt, double delay, string error);
226223

@@ -229,15 +226,15 @@ public static partial class Log
229226

230227
[LoggerMessage(Level = LogLevel.Error, Message = "Non-retryable error occurred while opening connection: {error}")]
231228
public static partial void FailedToOpenNonRetryableConnection(this ILogger logger, Exception exception, string error);
232-
233-
234-
235-
229+
236230
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to execute command on attempt {attempt}. Retrying in {delay}ms. Error: {error}")]
237231
public static partial void FailedToExecuteCommandRetry(this ILogger logger, int attempt, double delay, string error);
238232

239233
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to execute command after {totalAttempts} attempts.")]
240234
public static partial void FailedToExecuteCommandAfter(this ILogger logger, Exception exception, int totalAttempts);
241235
[LoggerMessage(Level = LogLevel.Error, Message = "Non-retryable error occurred while executing command: {error}")]
242236
public static partial void FailedToExecuteNonRetryableCommand(this ILogger logger, Exception exception, string error);
237+
238+
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set RATE LIMITER POLICY NAME to {name} by the comment annotation.")]
239+
public static partial void RateLimiterPolicySet(this ILogger logger, string description, string name);
243240
}

NpgsqlRest/NpgsqlRestMiddleware.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text.Json;
66
using System.Text.Json.Nodes;
77
using Microsoft.AspNetCore.Http.Extensions;
8+
using Microsoft.Extensions.Options;
89
using Microsoft.Extensions.Primitives;
910
using Npgsql;
1011
using NpgsqlRest.Auth;
@@ -82,9 +83,19 @@ public async Task InvokeAsync(HttpContext context)
8283
await next(context);
8384
return;
8485
}
85-
86+
87+
8688
Routine routine = entry.Endpoint.Routine;
8789
RoutineEndpoint endpoint = entry.Endpoint;
90+
91+
if (Options.RateLimiterOptions.Enabled is true && (endpoint.RateLimiterPolicy is not null || Options.RateLimiterOptions.DefaultPolicy is not null))
92+
{
93+
//var policyName = endpoint.RateLimiterPolicy ?? Options.RateLimiterOptions.DefaultPolicy!;
94+
//var rateLimiterOptionsAccessor = serviceProvider.GetService<IOptions<Microsoft.AspNetCore.RateLimiting.RateLimiterOptions>>();
95+
//var rateLimiterOptions = rateLimiterOptionsAccessor?.Value;
96+
//if (rateLimiterOptions is null || rateLimiterOptions.PolicyMap.TryGetValue(policyName
97+
}
98+
8899
IRoutineSourceParameterFormatter formatter = entry.Formatter;
89100

90101
string? headers = null;

NpgsqlRest/Options/NpgsqlRestOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,9 @@ public NpgsqlRestOptions(NpgsqlDataSource dataSource)
341341
/// Collection of custom server-sent events response headers that will be added to the response when connected to the endpoint that is configured to return server-sent events.
342342
/// </summary>
343343
public Dictionary<string, StringValues> CustomServerSentEventsResponseHeaders { get; set; } = [];
344+
345+
/// <summary>
346+
/// Rate Limiter Options
347+
/// </summary>
348+
public RateLimiterOptions RateLimiterOptions { get; set; } = new();
344349
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace NpgsqlRest;
2+
3+
public class RateLimiterOptions
4+
{
5+
/// <summary>
6+
/// Rate limiting is disabled by default.
7+
/// </summary>
8+
public bool Enabled { get; set; } = false;
9+
/// <summary>
10+
/// Status code returned when rate limit is exceeded. Default is 429 (Too Many Requests).
11+
/// </summary>
12+
public int StatusCode { get; set; } = 429;
13+
/// <summary>
14+
/// Message returned when rate limit is exceeded.
15+
/// </summary>
16+
public string? Message { get; set; } = "Too many requests. Please try again later.";
17+
/// <summary>
18+
/// Default rate limiting policy for all requests. Policy must be configured in RateLimitingOptions.
19+
/// This can be overridden by comment annotations in the database or setting policy for specific endpoints.
20+
/// </summary>
21+
public string? DefaultPolicy { get; set; } = null;
22+
}

NpgsqlRest/RoutineEndpoint.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ public class RoutineEndpoint(
3636
bool userParameters = false,
3737
string? infoEventsStreamingPath = null,
3838
InfoEventsScope infoEventsScope = InfoEventsScope.Self,
39-
HashSet<string>? infoEventsRoles = null,
40-
Auth.EndpointBasicAuthOptions? basicAuth = null)
39+
HashSet<string>? infoEventsRoles = null)
4140
{
4241
private string? _bodyParameterName = bodyParameterName;
4342

@@ -90,6 +89,7 @@ public string? BodyParameterName
9089
public string? InfoEventsStreamingPath { get; set; } = infoEventsStreamingPath;
9190
public InfoEventsScope InfoEventsScope { get; set; } = infoEventsScope;
9291
public HashSet<string>? InfoEventsRoles { get; set; } = infoEventsRoles;
93-
public Auth.EndpointBasicAuthOptions? BasicAuth { get; set; } = basicAuth;
92+
public Auth.EndpointBasicAuthOptions? BasicAuth { get; set; } = null;
9493
public RetryStrategy? RetryStrategy { get; set; } = null;
94+
public string? RateLimiterPolicy { get; set; } = null;
9595
}

NpgsqlRestClient/Builder.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.Collections.Frozen;
22
using System.IO.Compression;
3+
using System.Threading.RateLimiting;
34
using Microsoft.AspNetCore.Authentication.BearerToken;
45
using Microsoft.AspNetCore.Authentication.Cookies;
56
using Microsoft.AspNetCore.DataProtection;
67
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
78
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
89
using Microsoft.AspNetCore.DataProtection.KeyManagement;
10+
using Microsoft.AspNetCore.RateLimiting;
911
using Microsoft.AspNetCore.ResponseCompression;
1012
using Microsoft.Extensions.Primitives;
1113
using Microsoft.Net.Http.Headers;
@@ -14,6 +16,7 @@
1416
using Serilog;
1517
using Serilog.Extensions.Logging;
1618
using Serilog.Sinks.OpenTelemetry;
19+
using RateLimiterOptions = NpgsqlRest.RateLimiterOptions;
1720

1821
namespace NpgsqlRestClient;
1922

@@ -927,4 +930,114 @@ public CacheOptions BuildCacheOptions(WebApplication app)
927930

928931
return options;
929932
}
933+
934+
public enum RateLimiterType { FixedWindow, SlidingWindow, TokenBucket, Concurrency }
935+
936+
public RateLimiterOptions? BuildRateLimiter()
937+
{
938+
var rateLimiterCfg = _config.Cfg.GetSection("RateLimiterOptions");
939+
if (_config.Exists(rateLimiterCfg) is false || _config.GetConfigBool("Enabled", rateLimiterCfg) is false)
940+
{
941+
return null;
942+
}
943+
var policiesCfg = rateLimiterCfg.GetSection("Policies");
944+
if (_config.Exists(policiesCfg) is false)
945+
{
946+
return null;
947+
}
948+
949+
var result = new RateLimiterOptions
950+
{
951+
Enabled = true,
952+
DefaultPolicy = _config.GetConfigStr("DefaultPolicy", rateLimiterCfg),
953+
StatusCode = _config.GetConfigInt("StatusCode", rateLimiterCfg) ?? 429,
954+
Message = _config.GetConfigStr("Message", rateLimiterCfg) ?? "Too many requests. Please try again later."
955+
};
956+
957+
Instance.Services.AddRateLimiter(options =>
958+
{
959+
foreach (var sectionCfg in policiesCfg.GetChildren())
960+
{
961+
var type = _config.GetConfigEnum<RateLimiterType?>("Type", sectionCfg);
962+
if (type is null)
963+
{
964+
continue;
965+
}
966+
if (_config.GetConfigBool("Enabled", sectionCfg) is false)
967+
{
968+
continue;
969+
}
970+
var name = _config.GetConfigStr("Name", sectionCfg) ?? type.ToString()!;
971+
972+
if (type == RateLimiterType.FixedWindow)
973+
{
974+
options.AddFixedWindowLimiter(name, config =>
975+
{
976+
config.PermitLimit = _config.GetConfigInt("PermitLimit", sectionCfg) ?? 100;
977+
config.Window = TimeSpan.FromSeconds(_config.GetConfigInt("WindowSeconds", sectionCfg) ?? 60);
978+
config.QueueLimit = _config.GetConfigInt("QueueLimit", sectionCfg) ?? 10;
979+
config.AutoReplenishment = _config.GetConfigBool("AutoReplenishment", sectionCfg, true);
980+
});
981+
Logger?.LogDebug("Using Fixed Window rate limiter with name {Name}: PermitLimit={PermitLimit}, WindowSeconds={WindowSeconds}, QueueLimit={QueueLimit}, AutoReplenishment={AutoReplenishment}",
982+
name,
983+
_config.GetConfigInt("PermitLimit", sectionCfg) ?? 100,
984+
_config.GetConfigInt("WindowSeconds", sectionCfg) ?? 60,
985+
_config.GetConfigInt("QueueLimit", sectionCfg) ?? 10,
986+
_config.GetConfigBool("AutoReplenishment", sectionCfg, true));
987+
}
988+
else if (type == RateLimiterType.SlidingWindow)
989+
{
990+
options.AddSlidingWindowLimiter(name, config =>
991+
{
992+
config.PermitLimit = _config.GetConfigInt("PermitLimit", sectionCfg) ?? 100;
993+
config.Window = TimeSpan.FromSeconds(_config.GetConfigInt("WindowSeconds", sectionCfg) ?? 60);
994+
config.SegmentsPerWindow = _config.GetConfigInt("SegmentsPerWindow", sectionCfg) ?? 6;
995+
config.QueueLimit = _config.GetConfigInt("QueueLimit", sectionCfg) ?? 10;
996+
config.AutoReplenishment = _config.GetConfigBool("AutoReplenishment", sectionCfg, true);
997+
});
998+
Logger?.LogDebug("Using Sliding Window rate limiter with name {Name}: PermitLimit={PermitLimit}, WindowSeconds={WindowSeconds}, SegmentsPerWindow={SegmentsPerWindow}, QueueLimit={QueueLimit}, AutoReplenishment={AutoReplenishment}",
999+
name,
1000+
_config.GetConfigInt("PermitLimit", sectionCfg) ?? 100,
1001+
_config.GetConfigInt("WindowSeconds", sectionCfg) ?? 60,
1002+
_config.GetConfigInt("SegmentsPerWindow", sectionCfg) ?? 6,
1003+
_config.GetConfigInt("QueueLimit", sectionCfg) ?? 10,
1004+
_config.GetConfigBool("AutoReplenishment", sectionCfg, true));
1005+
}
1006+
else if (type == RateLimiterType.TokenBucket)
1007+
{
1008+
options.AddTokenBucketLimiter(name, config =>
1009+
{
1010+
config.TokenLimit = _config.GetConfigInt("TokenLimit", sectionCfg) ?? 100;
1011+
config.TokensPerPeriod = _config.GetConfigInt("TokensPerPeriod", sectionCfg) ?? 10;
1012+
config.ReplenishmentPeriod = TimeSpan.FromSeconds(_config.GetConfigInt("ReplenishmentPeriodSeconds", sectionCfg) ?? 10);
1013+
config.QueueLimit = _config.GetConfigInt("QueueLimit", sectionCfg) ?? 10;
1014+
config.AutoReplenishment = _config.GetConfigBool("AutoReplenishment", sectionCfg, true);
1015+
});
1016+
Logger?.LogDebug("Using Token Bucket rate limiter with name {Name}: TokenLimit={TokenLimit}, TokensPerPeriod={TokensPerPeriod}, ReplenishmentPeriodSeconds={ReplenishmentPeriodSeconds}, QueueLimit={QueueLimit}, AutoReplenishment={AutoReplenishment}",
1017+
name,
1018+
_config.GetConfigInt("TokenLimit", sectionCfg) ?? 100,
1019+
_config.GetConfigInt("TokensPerPeriod", sectionCfg) ?? 10,
1020+
_config.GetConfigInt("ReplenishmentPeriodSeconds", sectionCfg) ?? 1,
1021+
_config.GetConfigInt("QueueLimit", sectionCfg) ?? 10,
1022+
_config.GetConfigBool("AutoReplenishment", sectionCfg, true));
1023+
}
1024+
else if (type == RateLimiterType.Concurrency)
1025+
{
1026+
options.AddConcurrencyLimiter(name, config =>
1027+
{
1028+
config.PermitLimit = _config.GetConfigInt("PermitLimit", sectionCfg) ?? 100;
1029+
config.QueueLimit = _config.GetConfigInt("QueueLimit", sectionCfg) ?? 10;
1030+
config.QueueProcessingOrder = _config.GetConfigBool("OldestFirst", sectionCfg, true) is true ? QueueProcessingOrder.OldestFirst : QueueProcessingOrder.NewestFirst;
1031+
});
1032+
Logger?.LogDebug("Using Concurrency rate limiter with name {Name}: PermitLimit={PermitLimit}, QueueLimit={QueueLimit}, OldestFirst={OldestFirst}",
1033+
name,
1034+
_config.GetConfigInt("PermitLimit", sectionCfg) ?? 100,
1035+
_config.GetConfigInt("QueueLimit", sectionCfg) ?? 10,
1036+
_config.GetConfigBool("OldestFirst", sectionCfg, true));
1037+
}
1038+
}
1039+
});
1040+
1041+
return result;
1042+
}
9301043
}

NpgsqlRestClient/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114

115115
builder.BuildInstance();
116116
builder.BuildLogger(cmdRetryStrategy);
117+
var rateLimiterOptions = builder.BuildRateLimiter();
117118

118119
var (connectionString, retryOpts) = builder.BuildConnectionString();
119120
if (connectionString is null)
@@ -133,6 +134,11 @@
133134

134135
WebApplication app = builder.Build();
135136

137+
// if (rateLimiterOptions is not null && rateLimiterOptions.Enabled)
138+
// {
139+
// app.UseRateLimiter();
140+
// }
141+
136142
// dump encrypted text and exit
137143
if (args.Length >= 1 && string.Equals(args[0], "--encrypt", StringComparison.CurrentCultureIgnoreCase))
138144
{
@@ -243,7 +249,8 @@
243249
RefreshMethod = config.GetConfigStr("Method", refreshOptionsCfg) ?? "GET",
244250
UploadOptions = appInstance.CreateUploadOptions(),
245251

246-
CacheOptions = builder.BuildCacheOptions(app)
252+
CacheOptions = builder.BuildCacheOptions(app),
253+
RateLimiterOptions = rateLimiterOptions ?? new RateLimiterOptions(),
247254
};
248255

249256
app.UseNpgsqlRest(options);

NpgsqlRestClient/appsettings.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,54 @@
736736
//
737737
"RedisConfiguration": "localhost:6379,abortConnect=false,ssl=false,connectTimeout=10000,syncTimeout=5000,connectRetry=3"
738738
},
739+
740+
//
741+
// Rate Limiter settings to limit the number of requests from clients.
742+
//
743+
"RateLimiterOptions": {
744+
"Enabled": false,
745+
"StatusCode": 429,
746+
"StatusMessage": "Too many requests. Please try again later.",
747+
"DefaultPolicy": null,
748+
// Policy types: FixedWindow, SlidingWindow, BucketWindow, Concurrency
749+
"Policies": [{
750+
// see https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit#fixed
751+
"Type": "FixedWindow",
752+
"Enabled": false,
753+
"Name": "fixed",
754+
"PermitLimit": 100,
755+
"WindowSeconds": 60,
756+
"QueueLimit": 10,
757+
"AutoReplenishment": true
758+
}, {
759+
// see https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit#sliding-window-limiter
760+
"Type": "SlidingWindow",
761+
"Enabled": false,
762+
"Name": "sliding",
763+
"PermitLimit": 100,
764+
"WindowSeconds": 60,
765+
"SegmentsPerWindow": 6,
766+
"QueueLimit": 10,
767+
"AutoReplenishment": true
768+
}, {
769+
// see https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit#token-bucket-limiter
770+
"Type": "TokenBucket",
771+
"Enabled": true,
772+
"Name": "bucket",
773+
"TokenLimit": 100,
774+
"ReplenishmentPeriodSeconds": 10,
775+
"QueueLimit": 10,
776+
"AutoReplenishment": true
777+
}, {
778+
// see https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit#concurrency-limiter
779+
"Type": "Concurrency",
780+
"Enabled": true,
781+
"Name": "concurrency",
782+
"PermitLimit": 10,
783+
"QueueLimit": 5,
784+
"OldestFirst": true
785+
}]
786+
},
739787

740788
//
741789
// NpgsqlRest HTTP Middleware General Configuration

0 commit comments

Comments
 (0)