Skip to content

Commit

Permalink
extend existing .net rate limiting functionality to cover the use-cas…
Browse files Browse the repository at this point in the history
…es in the brief
  • Loading branch information
mhdubose committed Nov 12, 2024
1 parent d0a5741 commit eecff05
Show file tree
Hide file tree
Showing 16 changed files with 696 additions and 19 deletions.
24 changes: 23 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
.vs
packages
bin
obj
obj

**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/httpRequests/
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
*.suo
*.user
.vs/
[Bb]in/
[Oo]bj/
_UpgradeReport_Files/
[Pp]ackages/
13 changes: 13 additions & 0 deletions .idea/.idea.RateLimiter/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.RateLimiter/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.RateLimiter/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.RateLimiter/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/.idea.RateLimiter/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.RateLimiter/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

189 changes: 189 additions & 0 deletions RateLimiter.Tests/PredicatedRateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using System;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using NUnit.Framework;

namespace RateLimiter.Tests;

[TestFixture]
public class PredicatedRateLimiterTest
{
[Test]
public void CanCreatePredicatedRateLimiter()
{
var limiter = PredicatedRateLimiter.Create<int, int>(_ => true, _ => RateLimitPartition.GetNoLimiter(1));
Assert.That(limiter, Is.Not.Null);
}

[Test]
public void CanCreatePredicatedLimiterThatSkipsFailedPredicates()
{
var limiter = PredicatedRateLimiter.CreatePredicatedTimeSinceLimiter<int, int>(_ => false, 1,
_ => new TimeSinceLimiterOptions { AllowedPeriod = TimeSpan.FromMinutes(10) });

var lease = limiter.AttemptAcquire(1);
Assert.That(lease.IsAcquired, Is.True);
lease = limiter.AttemptAcquire(1);
Assert.That(lease.IsAcquired, Is.True);
}

[Test]
public void PredicatedLimiterLimitsPassedPredicates()
{
var limiter = PredicatedRateLimiter.CreatePredicatedTimeSinceLimiter<int, int>(_ => true, 1,
_ => new TimeSinceLimiterOptions { AllowedPeriod = TimeSpan.FromMinutes(10) });

var lease = limiter.AttemptAcquire(1);
Assert.That(lease.IsAcquired, Is.True);
lease = limiter.AttemptAcquire(1);

Assert.That(lease.IsAcquired, Is.False);
}

[Test]
public void DisposedLimiterThrows()
{
var limiter = PredicatedRateLimiter.Create<int, int>(_ => true, _ => RateLimitPartition.GetNoLimiter(1));
limiter.Dispose();
Assert.Throws<ObjectDisposedException>(() => limiter.AttemptAcquire(1));
}

[Test]
public void PredicateChainLimitsOnProperLimiter()
{
var mockLease = new Mock<RateLimitLease>();
mockLease.Setup(mock => mock.IsAcquired).Returns(true);
var firstLimiterMock = new Mock<System.Threading.RateLimiting.RateLimiter>();
firstLimiterMock.Protected().Setup<RateLimitLease>("AttemptAcquireCore", false, ItExpr.IsAny<int>())
.Returns(mockLease.Object).Verifiable();
var secondLimiterMock = new Mock<System.Threading.RateLimiting.RateLimiter>();
secondLimiterMock.Protected().Setup<RateLimitLease>("AttemptAcquireCore", false, ItExpr.IsAny<int>())
.Returns(mockLease.Object).Verifiable();
var limiter = PartitionedRateLimiter.CreateChained(
PredicatedRateLimiter.Create<int, int>(_ => false,
_ => RateLimitPartition.Get(1, _ => firstLimiterMock.Object)),
PredicatedRateLimiter.Create<int, int>(_ => true,
_ => RateLimitPartition.Get(1, _ => secondLimiterMock.Object))
);

var lease = limiter.AttemptAcquire(1);

firstLimiterMock.Protected().Verify("AttemptAcquireCore", Times.Never(), ItExpr.IsAny<int>());
secondLimiterMock.Protected().Verify("AttemptAcquireCore", Times.Once(), ItExpr.IsAny<int>());
}

[Test]
public void PredicateLeaseMetadataIsNull()
{
var limiter = PredicatedRateLimiter.Create<int, int>(_ => false, _ => RateLimitPartition.GetNoLimiter(1));

var lease = limiter.AttemptAcquire(1);

var success = lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter);
Assert.That(success, Is.False);
Assert.That(retryAfter, Is.Default);
}

[Test]
public void PredicateLeaseMetadataNamesIsEmpty()
{
var limiter = PredicatedRateLimiter.Create<int, int>(_ => false, _ => RateLimitPartition.GetNoLimiter(1));

var lease = limiter.AttemptAcquire(1);
var mataDataNames = lease.MetadataNames;

Assert.That(mataDataNames, Is.Empty);
}

[Test]
public void PredicateLimiterGetStatisticsReturnsUnderlyingStats()
{
var limiter = PartitionedRateLimiter.CreateChained(
PredicatedRateLimiter.CreatePredicatedTimeSinceLimiter<int, int>(_ => true, 1,
_ => new TimeSinceLimiterOptions { AllowedPeriod = TimeSpan.FromMinutes(10) }),
PredicatedRateLimiter.CreatePredicatedTimeSinceLimiter<int, int>(_ => true, 1,
_ => new TimeSinceLimiterOptions { AllowedPeriod = TimeSpan.FromMicroseconds(1) })
);

limiter.AttemptAcquire(1);
limiter.AttemptAcquire(1);
var stats = limiter.GetStatistics(1);

Assert.That(stats, Is.Not.Null);
Assert.That(stats?.CurrentAvailablePermits, Is.EqualTo(0));
Assert.That(stats?.TotalSuccessfulLeases, Is.EqualTo(1));
Assert.That(stats?.TotalFailedLeases, Is.EqualTo(1));
}

[Test]
public async Task PredicateLimiterCanAquireAsync()
{
var limiter = PredicatedRateLimiter.Create<int, int>(_ => true, _ => RateLimitPartition.GetNoLimiter(1));

var lease = await limiter.AcquireAsync(1);

Assert.That(lease.IsAcquired, Is.True);
}

[Test]
public async Task PredicateLimiterBypassesWhenAsync()
{
var limiter = PredicatedRateLimiter.CreatePredicatedTimeSinceLimiter<int, int>(_ => false, 1,
_ => new TimeSinceLimiterOptions { AllowedPeriod = TimeSpan.FromMinutes(10) });

var lease = await limiter.AcquireAsync(1);
Assert.That(lease.IsAcquired, Is.True);
lease = await limiter.AcquireAsync(1);
Assert.That(lease.IsAcquired, Is.True);
}

[Test]
public void CanCreatePredicatedTokenBucketLimiter()
{
var limiter = PredicatedRateLimiter.CreatePredicatedTokenBucketLimiter<int, int>(_ => true, 1, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = 1,
TokensPerPeriod = 1
});
Assert.That(limiter, Is.Not.Null);
}

[Test]
public void CanCreatePredicatedConcurrencyLimiter()
{
var limiter = PredicatedRateLimiter.CreatePredicatedConcurrencyLimiter<int, int>(_ => true, 1, _ =>
new ConcurrencyLimiterOptions
{
PermitLimit = 1
});
Assert.That(limiter, Is.Not.Null);
}

[Test]
public void CanCreatePredicatedSlidingWindowLimiter()
{
var limiter = PredicatedRateLimiter.CreatePredicatedSlidingWindowLimiter<int, int>(_ => true, 1, _ =>
new SlidingWindowRateLimiterOptions
{
PermitLimit = 1,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 1
});
Assert.That(limiter, Is.Not.Null);
}

[Test]
public void CanCreatePredicatedFixedWindowLimiter()
{
var limiter = PredicatedRateLimiter.CreatePredicatedFixedWindowLimiter<int, int>(_ => true, 1, _ =>
new FixedWindowRateLimiterOptions
{
PermitLimit = 1,
Window = TimeSpan.FromMinutes(1)
});
Assert.That(limiter, Is.Not.Null);
}
}
9 changes: 5 additions & 4 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
</Project>
13 changes: 0 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs

This file was deleted.

Loading

0 comments on commit eecff05

Please sign in to comment.