diff options
author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2022-09-03 03:03:47 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-03 03:03:47 +0300 |
commit | 8b5185a5dd56d7d55c61e7373f3b8483a9733602 (patch) | |
tree | 6380efa70316f1cfc639f39cef0eb03a4fc302bf /src | |
parent | d2e3961cffdab15659f54f431bef6e96c38e384c (diff) |
[release/7.0] [RateLimiting] Handle Timer jitter (#74971)
* [RateLimiting] TryReplenish handles multiple replenish periods at a time
* ignore tick on auto
* fixup
* partial
* allow TimeSpan.Zero
* no special case
* TimeSpan.Zero
* Apply suggestions from code review
Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Brennan Conroy <brecon@microsoft.com>
Co-authored-by: Stephen Halter <halter73@gmail.com>
Diffstat (limited to 'src')
9 files changed, 562 insertions, 359 deletions
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 fe4b0c29c3a..780eddd2cee 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 @@ namespace System.Threading.RateLimiting { 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 @@ namespace System.Threading.RateLimiting return; } - if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks) + if (((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks && !_options.AutoReplenishment) { return; } @@ -295,21 +295,14 @@ namespace System.Threading.RateLimiting _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 92cac84012c..8f7dbaa344b 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 @@ namespace System.Threading.RateLimiting { /// <summary> /// Specifies the time window that takes in the requests. - /// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="FixedWindowRateLimiter"/>. + /// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="FixedWindowRateLimiter"/>. /// </summary> 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 1ccf40775e2..5dfc3691448 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 @@ namespace System.Threading.RateLimiting private readonly Timer? _renewTimer; private readonly SlidingWindowRateLimiterOptions _options; + private readonly TimeSpan _replenishmentPeriod; private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>(); // 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 @@ namespace System.Threading.RateLimiting public override bool IsAutoReplenishing => _options.AutoReplenishment; /// <inheritdoc /> - public override TimeSpan ReplenishmentPeriod => new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow); + public override TimeSpan ReplenishmentPeriod => _replenishmentPeriod; /// <summary> /// Initializes the <see cref="SlidingWindowRateLimiter"/>. @@ -62,9 +63,9 @@ namespace System.Threading.RateLimiting { 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 @@ namespace System.Threading.RateLimiting }; _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 @@ namespace System.Threading.RateLimiting 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 8e1d397a57f..93f7ba933b4 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 @@ namespace System.Threading.RateLimiting { /// <summary> /// Specifies the minimum period between replenishments. - /// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="SlidingWindowRateLimiter"/>. + /// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="SlidingWindowRateLimiter"/>. /// </summary> 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 7baf91ea590..f1fbcb4433c 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 /// </summary> public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter { - private int _tokenCount; + private double _tokenCount; private int _queueCount; private long _lastReplenishmentTick; private long? _idleSince; @@ -22,6 +22,7 @@ namespace System.Threading.RateLimiting private long _failedLeasesCount; private long _successfulLeasesCount; + private readonly double _fillRate; private readonly Timer? _renewTimer; private readonly TokenBucketRateLimiterOptions _options; private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>(); @@ -60,9 +61,9 @@ namespace System.Threading.RateLimiting { 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 @@ namespace System.Threading.RateLimiting }; _tokenCount = options.TokenLimit; + _fillRate = (double)options.TokensPerPeriod / options.ReplenishmentPeriod.Ticks; _idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp(); @@ -91,7 +93,7 @@ namespace System.Threading.RateLimiting 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 @@ namespace System.Threading.RateLimiting 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 @@ namespace System.Threading.RateLimiting 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 @@ namespace System.Threading.RateLimiting 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<RequestRegistration> 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 @@ namespace System.Threading.RateLimiting { // 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 55b63f65d36..2c065d9432e 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 @@ namespace System.Threading.RateLimiting { /// <summary> /// Specifies the minimum period between replenishments. - /// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="TokenBucketRateLimiter"/>. + /// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="TokenBucketRateLimiter"/>. /// </summary> 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 6830a1ce742..1f597748d67 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -17,7 +17,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); var lease = limiter.AttemptAcquire(); @@ -27,7 +27,7 @@ namespace System.Threading.RateLimiting.Test lease.Dispose(); Assert.False(limiter.AttemptAcquire().IsAcquired); - Assert.True(limiter.TryReplenish()); + Replenish(limiter, 1L); Assert.True(limiter.AttemptAcquire().IsAcquired); } @@ -37,31 +37,49 @@ namespace System.Threading.RateLimiting.Test { Assert.Throws<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( + () => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + Window = TimeSpan.FromMinutes(-2), + AutoReplenishment = false, + })); + Assert.Throws<ArgumentException>( + () => new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + Window = TimeSpan.Zero, + AutoReplenishment = false, + })); } [Fact] @@ -72,7 +90,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -82,7 +100,7 @@ namespace System.Threading.RateLimiting.Test var wait = limiter.AcquireAsync(); Assert.False(wait.IsCompleted); - Assert.True(limiter.TryReplenish()); + Replenish(limiter, 1L); Assert.True((await wait).IsAcquired); } @@ -95,7 +113,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.True(limiter.TryReplenish()); + Replenish(limiter, 1L); lease = await wait1; Assert.True(lease.IsAcquired); @@ -115,7 +133,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - Window = TimeSpan.FromMinutes(0), + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -142,7 +160,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test Assert.False(lease1.IsAcquired); Assert.False(wait2.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait2; Assert.True(lease.IsAcquired); @@ -211,7 +229,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test Assert.False(lease2.IsAcquired); Assert.False(wait3.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait3; Assert.True(lease.IsAcquired); @@ -243,7 +261,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); Assert.Throws<ArgumentOutOfRangeException>(() => limiter.AttemptAcquire(2)); @@ -342,7 +360,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.AcquireAsync(2)); @@ -356,7 +374,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); Assert.Throws<ArgumentOutOfRangeException>(() => limiter.AttemptAcquire(-1)); @@ -370,7 +388,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.AcquireAsync(-1)); @@ -384,7 +402,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -400,7 +418,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -435,7 +453,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test wait = limiter.AcquireAsync(1); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); } @@ -568,7 +586,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -785,7 +803,7 @@ namespace System.Threading.RateLimiting.Test Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); @@ -799,7 +817,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -811,13 +829,13 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -845,7 +863,7 @@ namespace System.Threading.RateLimiting.Test Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); @@ -859,7 +877,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -872,7 +890,7 @@ namespace System.Threading.RateLimiting.Test lease = limiter.AttemptAcquire(1); Assert.False(lease.IsAcquired); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); @@ -918,11 +936,11 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test lease = await wait2; Assert.False(lease.IsAcquired); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait3; Assert.True(lease.IsAcquired); } @@ -1001,7 +1019,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -1049,7 +1067,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); @@ -1093,7 +1111,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), AutoReplenishment = false }); limiter.Dispose(); Assert.Throws<ObjectDisposedException>(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 7241a39e0f1..66e6cb2d5f2 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -17,7 +17,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -28,8 +28,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test { Assert.Throws<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( + () => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + Window = TimeSpan.FromMinutes(-2), + SegmentsPerWindow = 1, + AutoReplenishment = false + })); + Assert.Throws<ArgumentException>( + () => new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + Window = TimeSpan.Zero, + SegmentsPerWindow = 1, + AutoReplenishment = false + })); } [Fact] @@ -87,7 +107,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 4, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -98,14 +118,14 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 4, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 4, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(3), SegmentsPerWindow = 3, AutoReplenishment = false }); @@ -132,19 +152,19 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.FromMinutes(0), + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -172,10 +192,10 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - Window = TimeSpan.FromMinutes(0), + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -212,10 +232,10 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -257,7 +277,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -270,8 +290,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -304,8 +324,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -333,8 +353,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 3, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 2, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(3), SegmentsPerWindow = 3, AutoReplenishment = false }); @@ -358,20 +378,20 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = int.MaxValue, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = int.MaxValue, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -402,8 +422,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -431,7 +451,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -446,7 +466,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -461,7 +481,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -476,7 +496,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -493,7 +513,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -513,7 +533,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -530,7 +550,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -541,8 +561,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 4, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -568,8 +588,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -600,7 +620,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -627,7 +647,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -657,8 +677,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -689,7 +709,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -705,7 +725,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 1, AutoReplenishment = false }); @@ -739,7 +759,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -809,7 +829,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(3), SegmentsPerWindow = 3, AutoReplenishment = false }); @@ -825,12 +845,12 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 3, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 5, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -856,18 +876,18 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -896,8 +916,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -925,8 +945,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -1041,7 +1061,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test lease = await wait2; Assert.False(lease.IsAcquired); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait3; Assert.True(lease.IsAcquired); } @@ -1063,7 +1083,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(1), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -1089,7 +1109,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -1113,7 +1133,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(2), SegmentsPerWindow = 2, AutoReplenishment = false }); @@ -1138,7 +1158,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test Assert.Equal(2, stats.TotalFailedLeases); Assert.Equal(1, stats.TotalSuccessfulLeases); - limiter.TryReplenish(); + Replenish(limiter, 1); await lease2Task; stats = limiter.GetStatistics(); @@ -1174,7 +1194,7 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(3), SegmentsPerWindow = 3, AutoReplenishment = false }); @@ -1208,12 +1228,57 @@ namespace System.Threading.RateLimiting.Test PermitLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - Window = TimeSpan.Zero, + Window = TimeSpan.FromMilliseconds(3), SegmentsPerWindow = 3, AutoReplenishment = false }); limiter.Dispose(); Assert.Throws<ObjectDisposedException>(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 272c294a09b..79c368e2b6d 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs @@ -17,7 +17,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -26,9 +26,10 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test { Assert.Throws<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( () => 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<ArgumentException>( + () => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(-1), + TokensPerPeriod = 1, + AutoReplenishment = false + })); + Assert.Throws<ArgumentException>( + () => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 1, + ReplenishmentPeriod = TimeSpan.Zero, + TokensPerPeriod = 1, + AutoReplenishment = false + })); } [Fact] @@ -86,7 +107,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -97,7 +118,7 @@ namespace System.Threading.RateLimiting.Test var wait = limiter.AcquireAsync(); Assert.False(wait.IsCompleted); - Assert.True(limiter.TryReplenish()); + Replenish(limiter, 1L); Assert.True((await wait).IsAcquired); } @@ -110,7 +131,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 2, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -123,7 +144,7 @@ namespace System.Threading.RateLimiting.Test Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.True(limiter.TryReplenish()); + Replenish(limiter, 1L); lease = await wait1; Assert.True(lease.IsAcquired); @@ -131,7 +152,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.FromMinutes(0), + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -159,7 +180,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -193,7 +215,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -217,7 +239,7 @@ namespace System.Threading.RateLimiting.Test Assert.False(lease1.IsAcquired); Assert.False(wait2.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait2; Assert.True(lease.IsAcquired); @@ -231,7 +253,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -250,8 +272,8 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -279,7 +301,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -303,14 +325,14 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -368,7 +390,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -383,7 +405,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -398,7 +420,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -413,7 +435,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -430,7 +452,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -450,7 +472,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -467,7 +489,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -478,7 +500,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 2, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -504,7 +526,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -535,7 +557,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -575,7 +597,7 @@ namespace System.Threading.RateLimiting.Test lease = await wait2; Assert.False(lease.IsAcquired); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait3; Assert.True(lease.IsAcquired); } @@ -588,7 +610,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -614,7 +636,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -628,7 +650,7 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -658,7 +680,7 @@ namespace System.Threading.RateLimiting.Test wait = limiter.AcquireAsync(1); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); } @@ -671,7 +693,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -687,7 +709,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -703,7 +725,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -737,7 +759,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 1, AutoReplenishment = false }); @@ -888,14 +910,14 @@ namespace System.Threading.RateLimiting.Test } [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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 2, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -987,7 +1010,7 @@ namespace System.Threading.RateLimiting.Test Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); @@ -1001,7 +1024,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -1014,13 +1037,13 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -1049,7 +1072,7 @@ namespace System.Threading.RateLimiting.Test Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); - limiter.TryReplenish(); + Replenish(limiter, 1L); lease = await wait; Assert.True(lease.IsAcquired); @@ -1063,7 +1086,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 2, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 3, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -1077,14 +1100,12 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test 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 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 1, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 1, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 2, AutoReplenishment = false }); @@ -1238,7 +1259,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 30, AutoReplenishment = false }); @@ -1279,7 +1300,7 @@ namespace System.Threading.RateLimiting.Test Assert.Equal(2, stats.TotalFailedLeases); Assert.Equal(1, stats.TotalSuccessfulLeases); - limiter.TryReplenish(); + Replenish(limiter, 1); await lease2Task; stats = limiter.GetStatistics(); @@ -1297,7 +1318,7 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 30, AutoReplenishment = false }); @@ -1331,12 +1352,82 @@ namespace System.Threading.RateLimiting.Test TokenLimit = 100, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50, - ReplenishmentPeriod = TimeSpan.Zero, + ReplenishmentPeriod = TimeSpan.FromMilliseconds(1), TokensPerPeriod = 30, AutoReplenishment = false }); limiter.Dispose(); Assert.Throws<ObjectDisposedException>(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) }); + } } } |