Browse Source

feat: enhance operation rate limiting with current count tracking and reset behavior

pull/25024/head
maliming 7 months ago
parent
commit
471697841d
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 14
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingChecker.cs
  2. 2
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingRuleResult.cs
  3. 4
      framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Rules/FixedWindowOperationRateLimitingRule.cs
  4. 51
      framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/OperationRateLimitingChecker_Tests.cs

14
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingChecker.cs

@ -147,6 +147,11 @@ public class OperationRateLimitingChecker : IOperationRateLimitingChecker, ITran
public virtual async Task ResetAsync(string policyName, OperationRateLimitingContext? context = null) public virtual async Task ResetAsync(string policyName, OperationRateLimitingContext? context = null)
{ {
if (!Options.IsEnabled)
{
return;
}
context = EnsureContext(context); context = EnsureContext(context);
var policy = await PolicyProvider.GetAsync(policyName); var policy = await PolicyProvider.GetAsync(policyName);
var rules = CreateRules(policy); var rules = CreateRules(policy);
@ -168,12 +173,11 @@ public class OperationRateLimitingChecker : IOperationRateLimitingChecker, ITran
{ {
var rules = new List<IOperationRateLimitingRule>(); var rules = new List<IOperationRateLimitingRule>();
for (var i = 0; i < policy.Rules.Count; i++) foreach (var ruleDefinition in policy.Rules)
{ {
rules.Add(new FixedWindowOperationRateLimitingRule( rules.Add(new FixedWindowOperationRateLimitingRule(
policy.Name, policy.Name,
i, ruleDefinition,
policy.Rules[i],
Store, Store,
CurrentUser, CurrentUser,
CurrentTenant, CurrentTenant,
@ -203,7 +207,7 @@ public class OperationRateLimitingChecker : IOperationRateLimitingChecker, ITran
IsAllowed = isAllowed, IsAllowed = isAllowed,
RemainingCount = mostRestrictive.RemainingCount, RemainingCount = mostRestrictive.RemainingCount,
MaxCount = mostRestrictive.MaxCount, MaxCount = mostRestrictive.MaxCount,
CurrentCount = mostRestrictive.MaxCount - mostRestrictive.RemainingCount, CurrentCount = mostRestrictive.CurrentCount,
RetryAfter = ruleResults.Any(r => !r.IsAllowed && r.RetryAfter.HasValue) RetryAfter = ruleResults.Any(r => !r.IsAllowed && r.RetryAfter.HasValue)
? ruleResults ? ruleResults
.Where(r => !r.IsAllowed && r.RetryAfter.HasValue) .Where(r => !r.IsAllowed && r.RetryAfter.HasValue)
@ -248,7 +252,7 @@ public class OperationRateLimitingChecker : IOperationRateLimitingChecker, ITran
["IsAllowed"] = ruleResult.IsAllowed, ["IsAllowed"] = ruleResult.IsAllowed,
["MaxCount"] = ruleResult.MaxCount, ["MaxCount"] = ruleResult.MaxCount,
["RemainingCount"] = ruleResult.RemainingCount, ["RemainingCount"] = ruleResult.RemainingCount,
["CurrentCount"] = ruleResult.MaxCount - ruleResult.RemainingCount, ["CurrentCount"] = ruleResult.CurrentCount,
["WindowDurationSeconds"] = (int)ruleResult.WindowDuration.TotalSeconds, ["WindowDurationSeconds"] = (int)ruleResult.WindowDuration.TotalSeconds,
["WindowDescription"] = ruleResult.WindowDuration > TimeSpan.Zero ["WindowDescription"] = ruleResult.WindowDuration > TimeSpan.Zero
? formatter.Format(ruleResult.WindowDuration) ? formatter.Format(ruleResult.WindowDuration)

2
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Checker/OperationRateLimitingRuleResult.cs

@ -8,6 +8,8 @@ public class OperationRateLimitingRuleResult
public bool IsAllowed { get; set; } public bool IsAllowed { get; set; }
public int CurrentCount { get; set; }
public int RemainingCount { get; set; } public int RemainingCount { get; set; }
public int MaxCount { get; set; } public int MaxCount { get; set; }

4
framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Rules/FixedWindowOperationRateLimitingRule.cs

@ -10,7 +10,6 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
private const string HostTenantKey = "host"; private const string HostTenantKey = "host";
protected string PolicyName { get; } protected string PolicyName { get; }
protected int RuleIndex { get; }
protected OperationRateLimitingRuleDefinition Definition { get; } protected OperationRateLimitingRuleDefinition Definition { get; }
protected IOperationRateLimitingStore Store { get; } protected IOperationRateLimitingStore Store { get; }
protected ICurrentUser CurrentUser { get; } protected ICurrentUser CurrentUser { get; }
@ -19,7 +18,6 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
public FixedWindowOperationRateLimitingRule( public FixedWindowOperationRateLimitingRule(
string policyName, string policyName,
int ruleIndex,
OperationRateLimitingRuleDefinition definition, OperationRateLimitingRuleDefinition definition,
IOperationRateLimitingStore store, IOperationRateLimitingStore store,
ICurrentUser currentUser, ICurrentUser currentUser,
@ -27,7 +25,6 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
IWebClientInfoProvider webClientInfoProvider) IWebClientInfoProvider webClientInfoProvider)
{ {
PolicyName = policyName; PolicyName = policyName;
RuleIndex = ruleIndex;
Definition = definition; Definition = definition;
Store = store; Store = store;
CurrentUser = currentUser; CurrentUser = currentUser;
@ -140,6 +137,7 @@ public class FixedWindowOperationRateLimitingRule : IOperationRateLimitingRule
{ {
RuleName = $"{PolicyName}:Rule[{(long)Definition.Duration.TotalSeconds}s,{Definition.MaxCount},{Definition.PartitionType}]", RuleName = $"{PolicyName}:Rule[{(long)Definition.Duration.TotalSeconds}s,{Definition.MaxCount},{Definition.PartitionType}]",
IsAllowed = storeResult.IsAllowed, IsAllowed = storeResult.IsAllowed,
CurrentCount = storeResult.CurrentCount,
RemainingCount = storeResult.MaxCount - storeResult.CurrentCount, RemainingCount = storeResult.MaxCount - storeResult.CurrentCount,
MaxCount = storeResult.MaxCount, MaxCount = storeResult.MaxCount,
RetryAfter = storeResult.RetryAfter, RetryAfter = storeResult.RetryAfter,

51
framework/test/Volo.Abp.OperationRateLimiting.Tests/Volo/Abp/OperationRateLimiting/OperationRateLimitingChecker_Tests.cs

@ -731,6 +731,57 @@ public class OperationRateLimitingChecker_Tests : OperationRateLimitingTestBase
}); });
} }
[Fact]
public async Task Should_Return_Correct_CurrentCount_In_RuleResults()
{
var param = $"current-count-{Guid.NewGuid()}";
var context = new OperationRateLimitingContext { Parameter = param };
await _checker.CheckAsync("TestSimple", context);
await _checker.CheckAsync("TestSimple", context);
var status = await _checker.GetStatusAsync("TestSimple", context);
status.RuleResults.ShouldNotBeNull();
status.RuleResults!.Count.ShouldBe(1);
status.RuleResults[0].CurrentCount.ShouldBe(2);
status.RuleResults[0].RemainingCount.ShouldBe(1);
status.RuleResults[0].MaxCount.ShouldBe(3);
}
[Fact]
public async Task ResetAsync_Should_Skip_When_Disabled()
{
var options = GetRequiredService<Microsoft.Extensions.Options.IOptions<AbpOperationRateLimitingOptions>>();
var originalValue = options.Value.IsEnabled;
try
{
var param = $"reset-disabled-{Guid.NewGuid()}";
var context = new OperationRateLimitingContext { Parameter = param };
// Exhaust the quota
await _checker.CheckAsync("TestSimple", context);
await _checker.CheckAsync("TestSimple", context);
await _checker.CheckAsync("TestSimple", context);
// Disable and call ResetAsync — should be a no-op (counter not actually reset)
options.Value.IsEnabled = false;
await _checker.ResetAsync("TestSimple", context);
// Re-enable: quota should still be exhausted because reset was skipped
options.Value.IsEnabled = true;
await Assert.ThrowsAsync<AbpOperationRateLimitingException>(async () =>
{
await _checker.CheckAsync("TestSimple", context);
});
}
finally
{
options.Value.IsEnabled = originalValue;
}
}
private static ClaimsPrincipal CreateClaimsPrincipal(Guid userId) private static ClaimsPrincipal CreateClaimsPrincipal(Guid userId)
{ {
return new ClaimsPrincipal( return new ClaimsPrincipal(

Loading…
Cancel
Save