diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs
index fe4b0c29c3a62..780eddd2cee2e 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs
@@ -59,9 +59,9 @@ public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options)
{
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
}
- if (options.Window < TimeSpan.Zero)
+ if (options.Window <= TimeSpan.Zero)
{
- throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
+ throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
}
_options = new FixedWindowRateLimiterOptions
@@ -287,7 +287,7 @@ private void ReplenishInternal(long nowTicks)
return;
}
- if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks)
+ if (((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks && !_options.AutoReplenishment)
{
return;
}
@@ -295,21 +295,14 @@ private void ReplenishInternal(long nowTicks)
_lastReplenishmentTick = nowTicks;
int availableRequestCounters = _requestCount;
- int maxPermits = _options.PermitLimit;
- int resourcesToAdd;
- if (availableRequestCounters < maxPermits)
- {
- resourcesToAdd = maxPermits - availableRequestCounters;
- }
- else
+ if (availableRequestCounters >= _options.PermitLimit)
{
// All counters available, nothing to do
return;
}
- _requestCount += resourcesToAdd;
- Debug.Assert(_requestCount == _options.PermitLimit);
+ _requestCount = _options.PermitLimit;
// Process queued requests
while (_queue.Count > 0)
diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs
index 92cac84012c06..8f7dbaa344beb 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs
@@ -10,7 +10,7 @@ public sealed class FixedWindowRateLimiterOptions
{
///
/// Specifies the time window that takes in the requests.
- /// Must be set to a value >= by the time these options are passed to the constructor of .
+ /// Must be set to a value greater than by the time these options are passed to the constructor of .
///
public TimeSpan Window { get; set; } = TimeSpan.Zero;
diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs
index 1ccf40775e2d8..5dfc36914487e 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs
@@ -26,6 +26,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter
private readonly Timer? _renewTimer;
private readonly SlidingWindowRateLimiterOptions _options;
+ private readonly TimeSpan _replenishmentPeriod;
private readonly Deque _queue = new Deque();
// Use the queue as the lock field so we don't need to allocate another object for a lock and have another field in the object
@@ -42,7 +43,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter
public override bool IsAutoReplenishing => _options.AutoReplenishment;
///
- public override TimeSpan ReplenishmentPeriod => new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow);
+ public override TimeSpan ReplenishmentPeriod => _replenishmentPeriod;
///
/// Initializes the .
@@ -62,9 +63,9 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options)
{
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
}
- if (options.Window < TimeSpan.Zero)
+ if (options.Window <= TimeSpan.Zero)
{
- throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
+ throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
}
_options = new SlidingWindowRateLimiterOptions
@@ -78,6 +79,7 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options)
};
_requestCount = options.PermitLimit;
+ _replenishmentPeriod = new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow);
// _requestsPerSegment holds the no. of acquired requests in each window segment
_requestsPerSegment = new int[options.SegmentsPerWindow];
@@ -287,7 +289,7 @@ private void ReplenishInternal(long nowTicks)
return;
}
- if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < ReplenishmentPeriod.Ticks)
+ if (((nowTicks - _lastReplenishmentTick) * TickFrequency) < ReplenishmentPeriod.Ticks && !_options.AutoReplenishment)
{
return;
}
diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs
index 8e1d397a57f11..93f7ba933b464 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs
@@ -10,7 +10,7 @@ public sealed class SlidingWindowRateLimiterOptions
{
///
/// Specifies the minimum period between replenishments.
- /// Must be set to a value >= by the time these options are passed to the constructor of .
+ /// Must be set to a value greater than by the time these options are passed to the constructor of .
///
public TimeSpan Window { get; set; } = TimeSpan.Zero;
diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
index 7baf91ea59080..f1fbcb4433c4d 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
@@ -13,7 +13,7 @@ namespace System.Threading.RateLimiting
///
public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter
{
- private int _tokenCount;
+ private double _tokenCount;
private int _queueCount;
private long _lastReplenishmentTick;
private long? _idleSince;
@@ -22,6 +22,7 @@ public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter
private long _failedLeasesCount;
private long _successfulLeasesCount;
+ private readonly double _fillRate;
private readonly Timer? _renewTimer;
private readonly TokenBucketRateLimiterOptions _options;
private readonly Deque _queue = new Deque();
@@ -60,9 +61,9 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
{
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
}
- if (options.ReplenishmentPeriod < TimeSpan.Zero)
+ if (options.ReplenishmentPeriod <= TimeSpan.Zero)
{
- throw new ArgumentException($"{nameof(options.ReplenishmentPeriod)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
+ throw new ArgumentException($"{nameof(options.ReplenishmentPeriod)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
}
_options = new TokenBucketRateLimiterOptions
@@ -76,6 +77,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
};
_tokenCount = options.TokenLimit;
+ _fillRate = (double)options.TokensPerPeriod / options.ReplenishmentPeriod.Ticks;
_idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp();
@@ -91,7 +93,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
ThrowIfDisposed();
return new RateLimiterStatistics()
{
- CurrentAvailablePermits = _tokenCount,
+ CurrentAvailablePermits = (long)_tokenCount,
CurrentQueuedCount = _queueCount,
TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount),
TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount),
@@ -210,7 +212,7 @@ protected override ValueTask AcquireAsyncCore(int tokenCount, Ca
private RateLimitLease CreateFailedTokenLease(int tokenCount)
{
- int replenishAmount = tokenCount - _tokenCount + _queueCount;
+ int replenishAmount = tokenCount - (int)_tokenCount + _queueCount;
// can't have 0 replenish periods, that would mean it should be a successful lease
// if TokensPerPeriod is larger than the replenishAmount needed then it would be 0
Debug.Assert(_options.TokensPerPeriod > 0);
@@ -278,7 +280,7 @@ private static void Replenish(object? state)
limiter!.ReplenishInternal(nowTicks);
}
- // Used in tests that test behavior with specific time intervals
+ // Used in tests to avoid dealing with real time
private void ReplenishInternal(long nowTicks)
{
// method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes
@@ -289,37 +291,35 @@ private void ReplenishInternal(long nowTicks)
return;
}
- if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.ReplenishmentPeriod.Ticks)
+ if (_tokenCount == _options.TokenLimit)
{
return;
}
- _lastReplenishmentTick = nowTicks;
-
- int availablePermits = _tokenCount;
- TokenBucketRateLimiterOptions options = _options;
- int maxPermits = options.TokenLimit;
- int resourcesToAdd;
+ double add;
- if (availablePermits < maxPermits)
+ // Trust the timer to be close enough to when we want to replenish, this avoids issues with Timer jitter where it might be .99 seconds instead of 1, and 1.1 seconds the next time etc.
+ if (_options.AutoReplenishment)
{
- resourcesToAdd = Math.Min(options.TokensPerPeriod, maxPermits - availablePermits);
+ add = _options.TokensPerPeriod;
}
else
{
- // All tokens available, nothing to do
- return;
+ add = _fillRate * (nowTicks - _lastReplenishmentTick) * TickFrequency;
}
+ _tokenCount = Math.Min(_options.TokenLimit, _tokenCount + add);
+
+ _lastReplenishmentTick = nowTicks;
+
// Process queued requests
Deque queue = _queue;
- _tokenCount += resourcesToAdd;
Debug.Assert(_tokenCount <= _options.TokenLimit);
while (queue.Count > 0)
{
RequestRegistration nextPendingRequest =
- options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
? queue.PeekHead()
: queue.PeekTail();
@@ -327,7 +327,7 @@ private void ReplenishInternal(long nowTicks)
{
// Request can be fulfilled
nextPendingRequest =
- options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
? queue.DequeueHead()
: queue.DequeueTail();
diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs
index 55b63f65d36bc..2c065d9432e67 100644
--- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs
+++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs
@@ -10,7 +10,7 @@ public sealed class TokenBucketRateLimiterOptions
{
///
/// Specifies the minimum period between replenishments.
- /// Must be set to a value >= by the time these options are passed to the constructor of .
+ /// Must be set to a value greater than by the time these options are passed to the constructor of .
///
public TimeSpan ReplenishmentPeriod { get; set; } = TimeSpan.Zero;
diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs
index 6830a1ce74281..1f597748d67f3 100644
--- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs
+++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs
@@ -17,7 +17,7 @@ public override void CanAcquireResource()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire();
@@ -27,7 +27,7 @@ public override void CanAcquireResource()
lease.Dispose();
Assert.False(limiter.AttemptAcquire().IsAcquired);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True(limiter.AttemptAcquire().IsAcquired);
}
@@ -37,31 +37,49 @@ public override void InvalidOptionsThrows()
{
Assert.Throws(
() => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
- {
- PermitLimit = -1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- Window = TimeSpan.FromMinutes(2),
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = -1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.FromMinutes(2),
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
- {
- PermitLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = -1,
- Window = TimeSpan.FromMinutes(2),
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = -1,
+ Window = TimeSpan.FromMinutes(2),
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
- {
- PermitLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- Window = TimeSpan.MinValue,
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.MinValue,
+ AutoReplenishment = false
+ }));
+ Assert.Throws(
+ () => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.FromMinutes(-2),
+ AutoReplenishment = false,
+ }));
+ Assert.Throws(
+ () => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.Zero,
+ AutoReplenishment = false,
+ }));
}
[Fact]
@@ -72,7 +90,7 @@ public override async Task CanAcquireResourceAsync()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -82,7 +100,7 @@ public override async Task CanAcquireResourceAsync()
var wait = limiter.AcquireAsync();
Assert.False(wait.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True((await wait).IsAcquired);
}
@@ -95,7 +113,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = await limiter.AcquireAsync();
@@ -107,7 +125,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -115,7 +133,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
lease.Dispose();
Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -129,7 +147,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- Window = TimeSpan.FromMinutes(0),
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -142,7 +160,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
// second queued item completes first with NewestFirst
lease = await wait2;
@@ -151,7 +169,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
lease.Dispose();
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -165,7 +183,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
using var lease = limiter.AttemptAcquire(1);
@@ -174,7 +192,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst()
var failedLease = await limiter.AcquireAsync(1);
Assert.False(failedLease.IsAcquired);
Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan));
- Assert.Equal(TimeSpan.Zero, timeSpan);
+ Assert.Equal(TimeSpan.FromMilliseconds(2), timeSpan);
}
[Fact]
@@ -185,7 +203,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -197,7 +215,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
Assert.False(lease1.IsAcquired);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -211,7 +229,7 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(2);
@@ -229,7 +247,7 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
Assert.False(lease2.IsAcquired);
Assert.False(wait3.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
@@ -243,7 +261,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(2);
@@ -256,7 +274,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
var lease1 = await limiter.AcquireAsync(2);
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -270,7 +288,7 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -279,14 +297,14 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
var failedLease = await limiter.AcquireAsync(1);
Assert.False(failedLease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -299,7 +317,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
PermitLimit = int.MaxValue,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = int.MaxValue,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(int.MaxValue);
@@ -315,7 +333,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
var lease1 = await wait;
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
var lease2 = await wait2;
Assert.True(lease2.IsAcquired);
}
@@ -328,7 +346,7 @@ public override void ThrowsWhenAcquiringMoreThanLimit()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
Assert.Throws(() => limiter.AttemptAcquire(2));
@@ -342,7 +360,7 @@ public override async Task ThrowsWhenWaitingForMoreThanLimit()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
await Assert.ThrowsAsync(async () => await limiter.AcquireAsync(2));
@@ -356,7 +374,7 @@ public override void ThrowsWhenAcquiringLessThanZero()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
Assert.Throws(() => limiter.AttemptAcquire(-1));
@@ -370,7 +388,7 @@ public override async Task ThrowsWhenWaitingForLessThanZero()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
await Assert.ThrowsAsync(async () => await limiter.AcquireAsync(-1));
@@ -384,7 +402,7 @@ public override void AcquireZero_WithAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -400,7 +418,7 @@ public override void AcquireZero_WithoutAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
using var lease = limiter.AttemptAcquire(1);
@@ -419,7 +437,7 @@ public override async Task AcquireAsyncZero_WithAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -435,7 +453,7 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = await limiter.AcquireAsync(1);
@@ -445,7 +463,7 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
Assert.False(wait.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
using var lease2 = await wait;
Assert.True(lease2.IsAcquired);
}
@@ -458,7 +476,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
using var lease = await limiter.AcquireAsync(2);
@@ -470,7 +488,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
var lease1 = await wait1;
var lease2 = await wait2;
@@ -486,7 +504,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -500,7 +518,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -513,7 +531,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -526,7 +544,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -539,7 +557,7 @@ public override async Task CancelUpdatesQueueLimit()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -555,7 +573,7 @@ public override async Task CancelUpdatesQueueLimit()
wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -568,7 +586,7 @@ public override void NoMetadataOnAcquiredLease()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
using var lease = limiter.AttemptAcquire(1);
@@ -583,7 +601,7 @@ public override void MetadataNamesContainsAllMetadata()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
using var lease = limiter.AttemptAcquire(1);
@@ -598,7 +616,7 @@ public override async Task DisposeReleasesQueuedAcquires()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -631,7 +649,7 @@ public override async Task DisposeAsyncReleasesQueuedAcquires()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -770,7 +788,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -785,7 +803,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -799,7 +817,7 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -811,13 +829,13 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
Assert.False(wait.IsCompleted);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -831,7 +849,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -845,7 +863,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -859,7 +877,7 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -872,7 +890,7 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
lease = limiter.AttemptAcquire(1);
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -918,11 +936,11 @@ public override void IdleDurationUpdatesWhenChangingFromActive()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
limiter.AttemptAcquire(1);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
Assert.NotNull(limiter.IdleDuration);
}
@@ -962,7 +980,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(2);
@@ -988,7 +1006,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
lease = await wait2;
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
}
@@ -1001,7 +1019,7 @@ public override async Task CanDisposeAfterCancelingQueuedRequest()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(1);
@@ -1026,7 +1044,7 @@ public override void GetStatisticsReturnsNewInstances()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -1049,7 +1067,7 @@ public override async Task GetStatisticsHasCorrectValues()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
@@ -1093,7 +1111,7 @@ public override async Task GetStatisticsHasCorrectValues()
Assert.Equal(2, stats.TotalFailedLeases);
Assert.Equal(1, stats.TotalSuccessfulLeases);
- limiter.TryReplenish();
+ Replenish(limiter, 1);
await lease2Task;
// success from wait + available + queue
@@ -1112,7 +1130,7 @@ public override async Task GetStatisticsWithZeroPermitCount()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
var lease = limiter.AttemptAcquire(0);
@@ -1145,11 +1163,45 @@ public override void GetStatisticsThrowsAfterDispose()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
AutoReplenishment = false
});
limiter.Dispose();
Assert.Throws(limiter.GetStatistics);
}
+
+ [Fact]
+ public void AutoReplenishIgnoresTimerJitter()
+ {
+ var replenishmentPeriod = TimeSpan.FromMinutes(10);
+ using var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions
+ {
+ PermitLimit = 10,
+ QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
+ QueueLimit = 1,
+ Window = replenishmentPeriod,
+ AutoReplenishment = true,
+ });
+
+ var lease = limiter.AttemptAcquire(permitCount: 3);
+ Assert.True(lease.IsAcquired);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond less than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds - 1);
+
+ Assert.Equal(10, limiter.GetStatistics().CurrentAvailablePermits);
+ }
+
+ private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
+
+ static internal void Replenish(FixedWindowRateLimiter limiter, long addMilliseconds)
+ {
+ var replenishInternalMethod = typeof(FixedWindowRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var internalTick = typeof(FixedWindowRateLimiter).GetField("_lastReplenishmentTick", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var currentTick = (long)internalTick.GetValue(limiter);
+ replenishInternalMethod.Invoke(limiter, new object[] { currentTick + addMilliseconds * (long)(TimeSpan.TicksPerMillisecond / TickFrequency) });
+ }
}
}
diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs
index 7241a39e0f1f4..66e6cb2d5f228 100644
--- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs
+++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs
@@ -17,7 +17,7 @@ public override void CanAcquireResource()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -28,8 +28,8 @@ public override void CanAcquireResource()
lease.Dispose();
Assert.False(limiter.AttemptAcquire().IsAcquired);
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
Assert.True(limiter.AttemptAcquire().IsAcquired);
}
@@ -39,44 +39,64 @@ public override void InvalidOptionsThrows()
{
Assert.Throws(
() => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
- {
- PermitLimit = -1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- Window = TimeSpan.FromMinutes(2),
- SegmentsPerWindow = 1,
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = -1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.FromMinutes(2),
+ SegmentsPerWindow = 1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
- {
- PermitLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = -1,
- Window = TimeSpan.FromMinutes(2),
- SegmentsPerWindow = 1,
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = -1,
+ Window = TimeSpan.FromMinutes(2),
+ SegmentsPerWindow = 1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
- {
- PermitLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- Window = TimeSpan.FromMinutes(2),
- SegmentsPerWindow = -1,
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.FromMinutes(2),
+ SegmentsPerWindow = -1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
- {
- PermitLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- Window = TimeSpan.MinValue,
- SegmentsPerWindow = 1,
- AutoReplenishment = false
- }));
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.MinValue,
+ SegmentsPerWindow = 1,
+ AutoReplenishment = false
+ }));
+ Assert.Throws(
+ () => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.FromMinutes(-2),
+ SegmentsPerWindow = 1,
+ AutoReplenishment = false
+ }));
+ Assert.Throws(
+ () => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
+ {
+ PermitLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ Window = TimeSpan.Zero,
+ SegmentsPerWindow = 1,
+ AutoReplenishment = false
+ }));
}
[Fact]
@@ -87,7 +107,7 @@ public override async Task CanAcquireResourceAsync()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 4,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -98,14 +118,14 @@ public override async Task CanAcquireResourceAsync()
var wait = limiter.AcquireAsync(2);
Assert.False(wait.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
var wait2 = limiter.AcquireAsync(2);
Assert.False(wait2.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True((await wait2).IsAcquired);
}
@@ -121,7 +141,7 @@ public async Task CanAcquireMultipleRequestsAsync()
PermitLimit = 4,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 4,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(3),
SegmentsPerWindow = 3,
AutoReplenishment = false
});
@@ -132,19 +152,19 @@ public async Task CanAcquireMultipleRequestsAsync()
var wait = limiter.AcquireAsync(3);
Assert.False(wait.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
var wait2 = limiter.AcquireAsync(2);
Assert.True(wait2.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
var wait3 = limiter.AcquireAsync(2);
Assert.False(wait3.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True((await wait3).IsAcquired);
Assert.False((await wait).IsAcquired);
@@ -159,7 +179,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.FromMinutes(0),
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -172,10 +192,10 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.False(wait1.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -183,8 +203,8 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
lease.Dispose();
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -198,7 +218,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- Window = TimeSpan.FromMinutes(0),
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -212,10 +232,10 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.False(wait2.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
// second queued item completes first with NewestFirst
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -223,8 +243,8 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
lease.Dispose();
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -238,7 +258,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -257,7 +277,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -270,8 +290,8 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
Assert.False(lease1.IsAcquired);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -285,7 +305,7 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -304,8 +324,8 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
Assert.False(lease2.IsAcquired);
Assert.False(wait3.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
@@ -319,7 +339,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -333,8 +353,8 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
var lease1 = await limiter.AcquireAsync(2);
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -348,7 +368,7 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
PermitLimit = 3,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(3),
SegmentsPerWindow = 3,
AutoReplenishment = false
});
@@ -358,20 +378,20 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
var failedLease = await limiter.AcquireAsync(2);
Assert.False(failedLease.IsAcquired);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
wait = limiter.AcquireAsync(2);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -385,7 +405,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
PermitLimit = int.MaxValue,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = int.MaxValue,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -402,8 +422,8 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
var lease1 = await wait;
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
var lease2 = await wait2;
Assert.True(lease2.IsAcquired);
}
@@ -416,7 +436,7 @@ public override void ThrowsWhenAcquiringMoreThanLimit()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -431,7 +451,7 @@ public override async Task ThrowsWhenWaitingForMoreThanLimit()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -446,7 +466,7 @@ public override void ThrowsWhenAcquiringLessThanZero()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -461,7 +481,7 @@ public override async Task ThrowsWhenWaitingForLessThanZero()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -476,7 +496,7 @@ public override void AcquireZero_WithAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -493,7 +513,7 @@ public override void AcquireZero_WithoutAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -513,7 +533,7 @@ public override async Task AcquireAsyncZero_WithAvailability()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -530,7 +550,7 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -541,8 +561,8 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
Assert.False(wait.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
using var lease2 = await wait;
Assert.True(lease2.IsAcquired);
}
@@ -555,7 +575,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 4,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -568,8 +588,8 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
var lease1 = await wait1;
var lease2 = await wait2;
@@ -585,7 +605,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -600,7 +620,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -613,7 +633,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -627,7 +647,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -640,7 +660,7 @@ public override async Task CancelUpdatesQueueLimit()
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -657,8 +677,8 @@ public override async Task CancelUpdatesQueueLimit()
wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -673,7 +693,7 @@ public override void NoMetadataOnAcquiredLease()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -689,7 +709,7 @@ public override void MetadataNamesContainsAllMetadata()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -705,7 +725,7 @@ public override async Task DisposeReleasesQueuedAcquires()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 1,
AutoReplenishment = false
});
@@ -739,7 +759,7 @@ public override async Task DisposeAsyncReleasesQueuedAcquires()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -809,7 +829,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(3),
SegmentsPerWindow = 3,
AutoReplenishment = false
});
@@ -825,12 +845,12 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -843,7 +863,7 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
PermitLimit = 3,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 5,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -856,18 +876,18 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
Assert.False(wait.IsCompleted);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -881,7 +901,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -896,8 +916,8 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -911,7 +931,7 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -925,8 +945,8 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
lease = limiter.AttemptAcquire(1);
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -974,13 +994,13 @@ public override void IdleDurationUpdatesWhenChangingFromActive()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
limiter.AttemptAcquire(1);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
Assert.NotNull(limiter.IdleDuration);
}
@@ -1022,7 +1042,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -1041,7 +1061,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
var wait3 = limiter.AcquireAsync(2);
Assert.False(wait3.IsCompleted);
@@ -1050,7 +1070,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
lease = await wait2;
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
}
@@ -1063,7 +1083,7 @@ public override async Task CanDisposeAfterCancelingQueuedRequest()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(1),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -1089,7 +1109,7 @@ public override void GetStatisticsReturnsNewInstances()
PermitLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -1113,7 +1133,7 @@ public override async Task GetStatisticsHasCorrectValues()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(2),
SegmentsPerWindow = 2,
AutoReplenishment = false
});
@@ -1138,7 +1158,7 @@ public override async Task GetStatisticsHasCorrectValues()
Assert.Equal(0, stats.TotalFailedLeases);
Assert.Equal(1, stats.TotalSuccessfulLeases);
- limiter.TryReplenish();
+ Replenish(limiter, 1);
var lease3 = await limiter.AcquireAsync(1);
Assert.False(lease3.IsAcquired);
@@ -1156,7 +1176,7 @@ public override async Task GetStatisticsHasCorrectValues()
Assert.Equal(2, stats.TotalFailedLeases);
Assert.Equal(1, stats.TotalSuccessfulLeases);
- limiter.TryReplenish();
+ Replenish(limiter, 1);
await lease2Task;
stats = limiter.GetStatistics();
@@ -1174,7 +1194,7 @@ public override async Task GetStatisticsWithZeroPermitCount()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(3),
SegmentsPerWindow = 3,
AutoReplenishment = false
});
@@ -1208,12 +1228,57 @@ public override void GetStatisticsThrowsAfterDispose()
PermitLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- Window = TimeSpan.Zero,
+ Window = TimeSpan.FromMilliseconds(3),
SegmentsPerWindow = 3,
AutoReplenishment = false
});
limiter.Dispose();
Assert.Throws(limiter.GetStatistics);
}
+
+ [Fact]
+ public void AutoReplenishIgnoresTimerJitter()
+ {
+ var replenishmentPeriod = TimeSpan.FromMinutes(10);
+ using var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
+ {
+ PermitLimit = 10,
+ QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
+ QueueLimit = 1,
+ Window = replenishmentPeriod,
+ SegmentsPerWindow = 2,
+ AutoReplenishment = true,
+ });
+
+ var lease = limiter.AttemptAcquire(permitCount: 3);
+ Assert.True(lease.IsAcquired);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond less than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds / 2 - 1);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ lease = limiter.AttemptAcquire(permitCount: 3);
+ Assert.True(lease.IsAcquired);
+
+ Assert.Equal(4, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond longer than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds / 2 + 1);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+ }
+
+ private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
+
+ static internal void Replenish(SlidingWindowRateLimiter limiter, long addMilliseconds)
+ {
+ var replenishInternalMethod = typeof(SlidingWindowRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var internalTick = typeof(SlidingWindowRateLimiter).GetField("_lastReplenishmentTick", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var currentTick = (long)internalTick.GetValue(limiter);
+ replenishInternalMethod.Invoke(limiter, new object[] { currentTick + addMilliseconds * (long)(TimeSpan.TicksPerMillisecond / TickFrequency) });
+ }
}
}
diff --git a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs
index 272c294a09b34..79c368e2b6d34 100644
--- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs
+++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs
@@ -17,7 +17,7 @@ public override void CanAcquireResource()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -26,9 +26,10 @@ public override void CanAcquireResource()
Assert.True(lease.IsAcquired);
Assert.False(limiter.AttemptAcquire().IsAcquired);
+ // Dispose doesn't change token count
lease.Dispose();
Assert.False(limiter.AttemptAcquire().IsAcquired);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True(limiter.AttemptAcquire().IsAcquired);
}
@@ -38,44 +39,64 @@ public override void InvalidOptionsThrows()
{
Assert.Throws(
() => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
- {
- TokenLimit = -1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.FromMinutes(2),
- TokensPerPeriod = 1,
- AutoReplenishment = false
- }));
+ {
+ TokenLimit = -1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = TimeSpan.FromMinutes(2),
+ TokensPerPeriod = 1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
- {
- TokenLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = -1,
- ReplenishmentPeriod = TimeSpan.FromMinutes(2),
- TokensPerPeriod = 1,
- AutoReplenishment = false
- }));
+ {
+ TokenLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = -1,
+ ReplenishmentPeriod = TimeSpan.FromMinutes(2),
+ TokensPerPeriod = 1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
- {
- TokenLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.FromMinutes(2),
- TokensPerPeriod = -1,
- AutoReplenishment = false
- }));
+ {
+ TokenLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = TimeSpan.FromMinutes(2),
+ TokensPerPeriod = -1,
+ AutoReplenishment = false
+ }));
Assert.Throws(
() => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
- {
- TokenLimit = 1,
- QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
- QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.MinValue,
- TokensPerPeriod = 1,
- AutoReplenishment = false
- }));
+ {
+ TokenLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = TimeSpan.MinValue,
+ TokensPerPeriod = 1,
+ AutoReplenishment = false
+ }));
+ Assert.Throws(
+ () => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
+ {
+ TokenLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(-1),
+ TokensPerPeriod = 1,
+ AutoReplenishment = false
+ }));
+ Assert.Throws(
+ () => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
+ {
+ TokenLimit = 1,
+ QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = TimeSpan.Zero,
+ TokensPerPeriod = 1,
+ AutoReplenishment = false
+ }));
}
[Fact]
@@ -86,7 +107,7 @@ public override async Task CanAcquireResourceAsync()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -97,7 +118,7 @@ public override async Task CanAcquireResourceAsync()
var wait = limiter.AcquireAsync();
Assert.False(wait.IsCompleted);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.True((await wait).IsAcquired);
}
@@ -110,7 +131,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -123,7 +144,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -131,7 +152,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
lease.Dispose();
Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -145,7 +166,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.FromMinutes(0),
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -159,7 +180,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
// second queued item completes first with NewestFirst
lease = await wait2;
@@ -168,8 +189,9 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
lease.Dispose();
Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
+ Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
+ Replenish(limiter, 1L);
lease = await wait1;
Assert.True(lease.IsAcquired);
@@ -183,7 +205,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -193,7 +215,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst()
var failedLease = await limiter.AcquireAsync(1);
Assert.False(failedLease.IsAcquired);
Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan));
- Assert.Equal(TimeSpan.Zero, timeSpan);
+ Assert.Equal(TimeSpan.FromMilliseconds(2), timeSpan);
}
[Fact]
@@ -204,7 +226,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -217,7 +239,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst()
Assert.False(lease1.IsAcquired);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -231,7 +253,7 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -250,8 +272,8 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir
Assert.False(lease2.IsAcquired);
Assert.False(wait3.IsCompleted);
- limiter.TryReplenish();
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
@@ -265,7 +287,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -279,7 +301,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit
var lease1 = await limiter.AcquireAsync(2);
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -293,7 +315,7 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -303,14 +325,14 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv
var failedLease = await limiter.AcquireAsync(1);
Assert.False(failedLease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -323,7 +345,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
TokenLimit = int.MaxValue,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = int.MaxValue,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = int.MaxValue,
AutoReplenishment = false
});
@@ -340,7 +362,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow()
var lease1 = await wait;
Assert.False(lease1.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
var lease2 = await wait2;
Assert.True(lease2.IsAcquired);
}
@@ -353,7 +375,7 @@ public override void ThrowsWhenAcquiringMoreThanLimit()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -368,7 +390,7 @@ public override async Task ThrowsWhenWaitingForMoreThanLimit()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -383,7 +405,7 @@ public override void ThrowsWhenAcquiringLessThanZero()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -398,7 +420,7 @@ public override async Task ThrowsWhenWaitingForLessThanZero()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -413,7 +435,7 @@ public override void AcquireZero_WithAvailability()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -430,7 +452,7 @@ public override void AcquireZero_WithoutAvailability()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -450,7 +472,7 @@ public override async Task AcquireAsyncZero_WithAvailability()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -467,7 +489,7 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -478,7 +500,7 @@ public override async Task AcquireAsyncZero_WithoutAvailabilityWaitsForAvailabil
Assert.False(wait.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
using var lease2 = await wait;
Assert.True(lease2.IsAcquired);
}
@@ -491,7 +513,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -504,7 +526,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce()
Assert.False(wait2.IsCompleted);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
var lease1 = await wait1;
var lease2 = await wait2;
@@ -520,7 +542,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -535,7 +557,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -548,7 +570,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -575,7 +597,7 @@ public override async Task CanFillQueueWithNewestFirstAfterCancelingQueuedReques
lease = await wait2;
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait3;
Assert.True(lease.IsAcquired);
}
@@ -588,7 +610,7 @@ public override async Task CanDisposeAfterCancelingQueuedRequest()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -614,7 +636,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -628,7 +650,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing()
Assert.Equal(cts.Token, ex.CancellationToken);
lease.Dispose();
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -641,7 +663,7 @@ public override async Task CancelUpdatesQueueLimit()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -658,7 +680,7 @@ public override async Task CancelUpdatesQueueLimit()
wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -671,7 +693,7 @@ public override void NoMetadataOnAcquiredLease()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -687,7 +709,7 @@ public override void MetadataNamesContainsAllMetadata()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -703,7 +725,7 @@ public override async Task DisposeReleasesQueuedAcquires()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -737,7 +759,7 @@ public override async Task DisposeAsyncReleasesQueuedAcquires()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
@@ -888,14 +910,14 @@ public async Task CorrectRetryMetadataWithNonZeroAvailableItems()
}
[Fact]
- public void TryReplenishHonorsTokensPerPeriod()
+ public void ReplenishHonorsTokensPerPeriod()
{
var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 7,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 3,
AutoReplenishment = false
});
@@ -903,27 +925,28 @@ public void TryReplenishHonorsTokensPerPeriod()
Assert.False(limiter.AttemptAcquire(3).IsAcquired);
Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(5, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ Replenish(limiter, 1L);
Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
}
[Fact]
- public void TryReplenishWithAllTokensAvailable_Noops()
+ public async void TryReplenishWithAllTokensAvailable_Noops()
{
var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(30),
TokensPerPeriod = 1,
AutoReplenishment = false
});
Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits);
- Assert.True(limiter.TryReplenish());
+ await Task.Delay(100);
+ limiter.TryReplenish();
Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits);
}
@@ -971,7 +994,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -987,7 +1010,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -1001,7 +1024,7 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -1014,13 +1037,13 @@ public override async Task CannotAcquireResourcesWithAcquireAsyncWithQueuedItems
Assert.False(wait.IsCompleted);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
Assert.False(wait2.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait2;
Assert.True(lease.IsAcquired);
@@ -1034,7 +1057,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.NewestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -1049,7 +1072,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest
Assert.True(lease.IsAcquired);
Assert.False(wait.IsCompleted);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -1063,7 +1086,7 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
TokenLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -1077,14 +1100,12 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld
lease = limiter.AttemptAcquire(1);
Assert.False(lease.IsAcquired);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
- private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
-
[Fact]
public async Task ReplenishWorksWithTicksOverInt32Max()
{
@@ -1098,16 +1119,16 @@ public async Task ReplenishWorksWithTicksOverInt32Max()
AutoReplenishment = false
});
+ // Ensure next tick is over uint.MaxValue
+ Replenish(limiter, uint.MaxValue);
+
var lease = limiter.AttemptAcquire(10);
Assert.True(lease.IsAcquired);
var wait = limiter.AcquireAsync(1);
Assert.False(wait.IsCompleted);
- var replenishInternalMethod = typeof(TokenBucketRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
- // Ensure next tick is over uint.MaxValue
- var tick = Stopwatch.GetTimestamp() + uint.MaxValue;
- replenishInternalMethod.Invoke(limiter, new object[] { tick });
+ Replenish(limiter, 2L);
lease = await wait;
Assert.True(lease.IsAcquired);
@@ -1116,11 +1137,11 @@ public async Task ReplenishWorksWithTicksOverInt32Max()
Assert.False(wait.IsCompleted);
// Tick 1 millisecond too soon and verify that the queued item wasn't completed
- replenishInternalMethod.Invoke(limiter, new object[] { tick + 1L * (long)(TimeSpan.TicksPerMillisecond / TickFrequency) });
+ Replenish(limiter, 1L);
Assert.False(wait.IsCompleted);
// ticks would wrap if using uint
- replenishInternalMethod.Invoke(limiter, new object[] { tick + 2L * (long)(TimeSpan.TicksPerMillisecond / TickFrequency) });
+ Replenish(limiter, 2L);
lease = await wait;
Assert.True(lease.IsAcquired);
}
@@ -1167,12 +1188,12 @@ public override void IdleDurationUpdatesWhenChangingFromActive()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 1,
AutoReplenishment = false
});
limiter.AttemptAcquire(1);
- limiter.TryReplenish();
+ Replenish(limiter, 1L);
Assert.NotNull(limiter.IdleDuration);
}
@@ -1214,7 +1235,7 @@ public override void GetStatisticsReturnsNewInstances()
TokenLimit = 1,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 2,
AutoReplenishment = false
});
@@ -1238,7 +1259,7 @@ public override async Task GetStatisticsHasCorrectValues()
TokenLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 30,
AutoReplenishment = false
});
@@ -1279,7 +1300,7 @@ public override async Task GetStatisticsHasCorrectValues()
Assert.Equal(2, stats.TotalFailedLeases);
Assert.Equal(1, stats.TotalSuccessfulLeases);
- limiter.TryReplenish();
+ Replenish(limiter, 1);
await lease2Task;
stats = limiter.GetStatistics();
@@ -1297,7 +1318,7 @@ public override async Task GetStatisticsWithZeroPermitCount()
TokenLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 30,
AutoReplenishment = false
});
@@ -1331,12 +1352,82 @@ public override void GetStatisticsThrowsAfterDispose()
TokenLimit = 100,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
- ReplenishmentPeriod = TimeSpan.Zero,
+ ReplenishmentPeriod = TimeSpan.FromMilliseconds(1),
TokensPerPeriod = 30,
AutoReplenishment = false
});
limiter.Dispose();
Assert.Throws(limiter.GetStatistics);
}
+
+ [Fact]
+ public void AutoReplenishIgnoresTimerJitter()
+ {
+ var replenishmentPeriod = TimeSpan.FromMinutes(10);
+ using var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
+ {
+ TokenLimit = 10,
+ QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = replenishmentPeriod,
+ AutoReplenishment = true,
+ TokensPerPeriod = 1,
+ });
+
+ var lease = limiter.AttemptAcquire(permitCount: 3);
+ Assert.True(lease.IsAcquired);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond less than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds - 1);
+
+ Assert.Equal(8, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond longer than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds + 1);
+
+ Assert.Equal(9, limiter.GetStatistics().CurrentAvailablePermits);
+ }
+
+ [Fact]
+ public void ManualReplenishPreservesTimeWithTimerJitter()
+ {
+ var replenishmentPeriod = TimeSpan.FromMinutes(10);
+ using var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
+ {
+ TokenLimit = 10,
+ QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
+ QueueLimit = 1,
+ ReplenishmentPeriod = replenishmentPeriod,
+ AutoReplenishment = false,
+ TokensPerPeriod = 1,
+ });
+
+ var lease = limiter.AttemptAcquire(permitCount: 3);
+ Assert.True(lease.IsAcquired);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond less than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds - 1);
+
+ Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits);
+
+ // Replenish 1 millisecond longer than ReplenishmentPeriod while AutoReplenishment is enabled
+ Replenish(limiter, (long)replenishmentPeriod.TotalMilliseconds + 1);
+
+ Assert.Equal(9, limiter.GetStatistics().CurrentAvailablePermits);
+ }
+
+ private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
+
+ static internal void Replenish(TokenBucketRateLimiter limiter, long addMilliseconds)
+ {
+ var replenishInternalMethod = typeof(TokenBucketRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var internalTick = typeof(TokenBucketRateLimiter).GetField("_lastReplenishmentTick", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ var currentTick = (long)internalTick.GetValue(limiter);
+ replenishInternalMethod.Invoke(limiter, new object[] { currentTick + addMilliseconds * (long)(TimeSpan.TicksPerMillisecond / TickFrequency) });
+ }
}
}