From 5c8840c7e021e31d0c93c9dca4a00735a29a4b3f Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Wed, 23 Feb 2022 16:18:44 -0800 Subject: [PATCH 01/12] Add RL non-generic fixed window, sliding window implementations --- .../ref/System.Threading.RateLimiting.cs | 45 ++ .../src/System.Threading.RateLimiting.csproj | 6 +- .../RateLimiting/FixedWindowRateLimiter.cs | 427 +++++++++++ .../FixedWindowRateLimiterOptions.cs | 77 ++ .../RateLimiting/SlidingWindowRateLimiter.cs | 415 +++++++++++ .../SlidingWindowRateLimiterOptions.cs | 89 +++ .../tests/FixedWindowRateLimiterTests.cs | 694 +++++++++++++++++ .../tests/SlidingWindowRateLimiterTests.cs | 696 ++++++++++++++++++ ...System.Threading.RateLimiting.Tests.csproj | 2 + 9 files changed, 2450 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs create mode 100644 src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs create mode 100644 src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs diff --git a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs index 13e779c61ca797..7f1dbb49940a5d 100644 --- a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs +++ b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs @@ -113,4 +113,49 @@ public TokenBucketRateLimiterOptions(int tokenLimit, System.Threading.RateLimiti public int TokenLimit { get { throw null; } } public int TokensPerPeriod { get { throw null; } } } + public sealed partial class SlidingWindowRateLimiter : System.Threading.RateLimiting.ReplenishingRateLimiter + { + public SlidingWindowRateLimiter(System.Threading.RateLimiting.SlidingWindowRateLimiterOptions options) { } + public override System.TimeSpan? IdleDuration { get { throw null; } } + public override bool IsAutoReplenishing { get { throw null; } } + public override System.TimeSpan ReplenishmentPeriod { get { throw null; } } + protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount) { throw null; } + protected override void Dispose(bool disposing) { } + protected override System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } + public override int GetAvailablePermits() { throw null; } + public override bool TryReplenish() { throw null; } + protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public sealed partial class SlidingWindowRateLimiterOptions + { + public SlidingWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, int segmentsPerWindow, bool autoRefresh = true) { } + public bool AutoReplenishment { get { throw null; } } + public int QueueLimit { get { throw null; } } + public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } } + public System.TimeSpan Window { get { throw null; } } + public int PermitLimit { get { throw null; } } + public int SegmentsPerWindow { get { throw null; } } + } + public sealed partial class FixedWindowRateLimiter : System.Threading.RateLimiting.ReplenishingRateLimiter + { + public FixedWindowRateLimiter(System.Threading.RateLimiting.FixedWindowRateLimiterOptions options) { } + public override System.TimeSpan? IdleDuration { get { throw null; } } + public override bool IsAutoReplenishing { get { throw null; } } + public override System.TimeSpan ReplenishmentPeriod { get { throw null; } } + protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount) { throw null; } + protected override void Dispose(bool disposing) { } + protected override System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } + public override int GetAvailablePermits() { throw null; } + public override bool TryReplenish() { throw null; } + protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public sealed partial class FixedWindowRateLimiterOptions + { + public FixedWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, bool autoRefresh = true) { } + public bool AutoReplenishment { get { throw null; } } + public int QueueLimit { get { throw null; } } + public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } } + public System.TimeSpan Window { get { throw null; } } + public int PermitLimit { get { throw null; } } + } } diff --git a/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj b/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj index 90582d65edee1a..edcf6445f88cab 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj +++ b/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) true @@ -16,6 +16,8 @@ System.Threading.RateLimiting.RateLimitLease + + @@ -23,6 +25,8 @@ System.Threading.RateLimiting.RateLimitLease + + 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 new file mode 100644 index 00000000000000..4bd4521cddf8c2 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs @@ -0,0 +1,427 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace System.Threading.RateLimiting +{ + /// + /// implementation that refreshes allowed permits in a window periodically. + /// + public sealed class FixedWindowRateLimiter : ReplenishingRateLimiter + { + private int _requestCount; + private int _queueCount; + private long _lastReplenishmentTick; + private long? _idleSince; + private bool _disposed; + + private readonly Timer? _renewTimer; + private readonly FixedWindowRateLimiterOptions _options; + private readonly Deque _queue = new Deque(); + + private object Lock => _queue; + + private static readonly RateLimitLease SuccessfulLease = new FixedWindowLease(true, null); + private static readonly RateLimitLease FailedLease = new FixedWindowLease(false, null); + private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency; + + /// + public override TimeSpan? IdleDuration => _idleSince is null ? null : new TimeSpan((long)((Stopwatch.GetTimestamp() - _idleSince) * TickFrequency)); + + /// + public override bool IsAutoReplenishing => _options.AutoReplenishment; + + /// + public override TimeSpan ReplenishmentPeriod => _options.Window; + + + + /// + /// Initializes the . + /// + /// Options to specify the behavior of the . + public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options!!) + { + _options = options; + _requestCount = options.PermitLimit; + + _idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp(); + + if (_options.AutoReplenishment) + { + _renewTimer = new Timer(Replenish, this, _options.Window, _options.Window); + } + } + + /// + public override int GetAvailablePermits() => _requestCount; + + /// + protected override RateLimitLease AcquireCore(int requestCount) + { + // These amounts of resources can never be acquired + // Raises a PermitLimitExceeded ArgumentOutOFRangeException + if (requestCount > _options.PermitLimit) + { + throw new ArgumentOutOfRangeException(nameof(requestCount), requestCount, SR.Format(SR.PermitLimitExceeded, requestCount, _options.PermitLimit)); + } + + // Return SuccessfulLease or FailedLease depending to indicate limiter state + if (requestCount == 0 && !_disposed) + { + // Check if the requests are permitted in a window + // Requests will be allowed if the total served request is the max allowed requests (permit limit). + if (_requestCount > 0) + { + return SuccessfulLease; + } + + return CreateFailedWindowLease(requestCount); + } + + lock (Lock) + { + if (TryLeaseUnsynchronized(requestCount, out RateLimitLease? lease)) + { + return lease; + } + + return CreateFailedWindowLease(requestCount); + } + } + + /// + protected override ValueTask WaitAsyncCore(int requestCount, CancellationToken cancellationToken = default) + { + // These amounts of resources can never be acquired + if (requestCount > _options.PermitLimit) + { + throw new ArgumentOutOfRangeException(nameof(requestCount), requestCount, SR.Format(SR.PermitLimitExceeded, requestCount, _options.PermitLimit)); + } + + ThrowIfDisposed(); + + // Return SuccessfulAcquisition if requestedCount is 0 and resources are available + if (requestCount == 0 && _requestCount > 0) + { + return new ValueTask(SuccessfulLease); + } + + lock (Lock) + { + if (TryLeaseUnsynchronized(requestCount, out RateLimitLease? lease)) + { + return new ValueTask(lease); + } + + // Avoid integer overflow by using subtraction instead of addition + Debug.Assert(_options.QueueLimit >= _queueCount); + if (_options.QueueLimit - _queueCount < requestCount) + { + if (_options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst && requestCount <= _options.QueueLimit) + { + // remove oldest items from queue until there is space for the newest acquisition request + do + { + RequestRegistration oldestRequest = _queue.DequeueHead(); + _queueCount -= oldestRequest.Count; + Debug.Assert(_queueCount >= 0); + oldestRequest.Tcs.TrySetResult(FailedLease); + } + while (_options.QueueLimit - _queueCount < requestCount); + } + else + { + // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst + return new ValueTask(CreateFailedWindowLease(requestCount)); + } + } + + CancelQueueState tcs = new CancelQueueState(requestCount, this, cancellationToken); + CancellationTokenRegistration ctr = default; + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.Register(static obj => + { + ((CancelQueueState)obj!).TrySetCanceled(); + }, tcs); + } + + RequestRegistration registration = new RequestRegistration(requestCount, tcs, ctr); + _queue.EnqueueTail(registration); + _queueCount += requestCount; + Debug.Assert(_queueCount <= _options.QueueLimit); + + return new ValueTask(registration.Tcs.Task); + } + } + + private RateLimitLease CreateFailedWindowLease(int requestCount) + { + int replenishAmount = requestCount - _requestCount + _queueCount; + // can't have 0 replenish window, that would mean it should be a successful lease + int replenishWindow = Math.Max(replenishAmount / _options.PermitLimit, 1); + + return new FixedWindowLease(false, TimeSpan.FromTicks(_options.Window.Ticks * replenishWindow)); + } + + private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out RateLimitLease? lease) + { + ThrowIfDisposed(); + + // if permitCount is 0 we want to queue it if there are no available permits + if (_requestCount >= requestCount && _requestCount != 0) + { + if (requestCount == 0) + { + // Edge case where the check before the lock showed 0 available permit counters but when we got the lock, some permits were now available + lease = SuccessfulLease; + return true; + } + + // a. if there are no items queued we can lease + // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest + if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst)) + { + _idleSince = null; + _requestCount -= requestCount; + Debug.Assert(_requestCount >= 0); + lease = SuccessfulLease; + return true; + } + } + + lease = null; + return false; + } + + /// + /// Attempts to replenish request counters in the window. + /// + /// + /// False if is enabled, otherwise true. + /// Does not reflect if counters were replenished. + /// + public override bool TryReplenish() + { + if (_options.AutoReplenishment) + { + return false; + } + Replenish(this); + return true; + } + + private static void Replenish(object? state) + { + FixedWindowRateLimiter limiter = (state as FixedWindowRateLimiter)!; + Debug.Assert(limiter is not null); + + // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change + long nowTicks = Stopwatch.GetTimestamp(); + limiter!.ReplenishInternal(nowTicks); + } + + // Used in tests that test behavior with specific time intervals + private void ReplenishInternal(long nowTicks) + { + // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes + lock (Lock) + { + if (_disposed) + { + return; + } + + if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks) + { + return; + } + + _lastReplenishmentTick = nowTicks; + + int availableRequestCounters = _requestCount; + FixedWindowRateLimiterOptions options = _options; + int maxPermits = options.PermitLimit; + int resourcesToAdd; + + if (availableRequestCounters < maxPermits) + { + resourcesToAdd = maxPermits - availableRequestCounters; + } + else + { + // All counters available, nothing to do + return; + } + + // Process queued requests + Deque queue = _queue; + + _requestCount += resourcesToAdd; + Debug.Assert(_requestCount <= _options.PermitLimit); + while (queue.Count > 0) + { + RequestRegistration nextPendingRequest = + options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? queue.PeekHead() + : queue.PeekTail(); + + if (_requestCount >= nextPendingRequest.Count) + { + // Request can be fulfilled + nextPendingRequest = + options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? queue.DequeueHead() + : queue.DequeueTail(); + + _queueCount -= nextPendingRequest.Count; + _requestCount -= nextPendingRequest.Count; + Debug.Assert(_requestCount >= 0); + + if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease)) + { + // Queued item was canceled so add count back + _requestCount += nextPendingRequest.Count; + // Updating queue count is handled by the cancellation code + _queueCount += nextPendingRequest.Count; + } + nextPendingRequest.CancellationTokenRegistration.Dispose(); + Debug.Assert(_queueCount >= 0); + } + else + { + // Request cannot be fulfilled + break; + } + } + + if (_requestCount == _options.PermitLimit) + { + Debug.Assert(_idleSince is null); + Debug.Assert(_queueCount == 0); + _idleSince = Stopwatch.GetTimestamp(); + } + } + } + + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + lock (Lock) + { + if (_disposed) + { + return; + } + _disposed = true; + _renewTimer?.Dispose(); + while (_queue.Count > 0) + { + RequestRegistration next = _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.DequeueHead() + : _queue.DequeueTail(); + next.CancellationTokenRegistration.Dispose(); + next.Tcs.SetResult(FailedLease); + } + } + } + + protected override ValueTask DisposeAsyncCore() + { + Dispose(true); + + return default; + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(FixedWindowRateLimiter)); + } + } + + private sealed class FixedWindowLease : RateLimitLease + { + private static readonly string[] s_allMetadataNames = new[] { MetadataName.RetryAfter.Name }; + + private readonly TimeSpan? _retryAfter; + + public FixedWindowLease(bool isAcquired, TimeSpan? retryAfter) + { + IsAcquired = isAcquired; + _retryAfter = retryAfter; + } + + public override bool IsAcquired { get; } + + public override IEnumerable MetadataNames => s_allMetadataNames; + + public override bool TryGetMetadata(string metadataName, out object? metadata) + { + if (metadataName == MetadataName.RetryAfter.Name && _retryAfter.HasValue) + { + metadata = _retryAfter.Value; + return true; + } + + metadata = default; + return false; + } + } + + private readonly struct RequestRegistration + { + public RequestRegistration(int requestCount, TaskCompletionSource tcs, CancellationTokenRegistration cancellationTokenRegistration) + { + Count = requestCount; + // Use VoidAsyncOperationWithData instead + Tcs = tcs; + CancellationTokenRegistration = cancellationTokenRegistration; + } + + public int Count { get; } + + public TaskCompletionSource Tcs { get; } + + public CancellationTokenRegistration CancellationTokenRegistration { get; } + } + + private sealed class CancelQueueState : TaskCompletionSource + { + private readonly int _requestCount; + private readonly FixedWindowRateLimiter _limiter; + private readonly CancellationToken _cancellationToken; + + public CancelQueueState(int requestCount, FixedWindowRateLimiter limiter, CancellationToken cancellationToken) + : base(TaskCreationOptions.RunContinuationsAsynchronously) + { + _requestCount = requestCount; + _limiter = limiter; + _cancellationToken = cancellationToken; + } + + public new bool TrySetCanceled() + { + if (TrySetCanceled(_cancellationToken)) + { + lock (_limiter.Lock) + { + _limiter._queueCount -= _requestCount; + } + return true; + } + return false; + } + } + } +} 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 new file mode 100644 index 00000000000000..ef6c035de84058 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.RateLimiting +{ + /// + /// Options to specify the behavior of a . + /// + public sealed class FixedWindowRateLimiterOptions + { + /// + /// Initializes the . + /// + /// Maximum number of requests that can be served in the window. + /// + /// Maximum number of unprocessed request counters waiting via . + /// + /// Specifies how often request counters can be replenished. Replenishing is triggered either by an internal timer if is true, or by calling . + /// + /// + /// Specifies whether request replenishment will be handled by the or by another party via . + /// + /// When or are less than 0 + /// or when is more than 49 days. + public FixedWindowRateLimiterOptions( + int permitLimit, + QueueProcessingOrder queueProcessingOrder, + int queueLimit, + TimeSpan window, + bool autoReplenishment = true) + { + if (permitLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(permitLimit)); + } + if (queueLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(queueLimit)); + } + + PermitLimit = permitLimit; + QueueProcessingOrder = queueProcessingOrder; + QueueLimit = queueLimit; + Window = window; + AutoReplenishment = autoReplenishment; + } + + /// + /// Specifies the time window that takes in the requests. + /// + public TimeSpan Window { get; } + + /// + /// Specified whether the is automatically refresh counters or if someone else + /// will be calling to refresh counters. + /// + public bool AutoReplenishment { get; } + + /// + /// Maximum number of permit counters that can be allowed in a window. + /// + public int PermitLimit { get; } + + /// + /// Determines the behaviour of when not enough resources can be leased. + /// + /// + /// by default. + /// + public QueueProcessingOrder QueueProcessingOrder { get; } + + /// + /// Maximum cumulative permit count of queued acquisition requests. + /// + public int QueueLimit { get; } + } +} 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 new file mode 100644 index 00000000000000..2a4f48c2a66796 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs @@ -0,0 +1,415 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace System.Threading.RateLimiting +{ + /// + /// implementation that replenishes permit counters periodically instead of via a release mechanism. + /// + public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter + { + private int _requestCount; + private int _queueCount; + private int[] _requestsPerSegment; + private int _currentSegmentIndex; + private long _lastReplenishmentTick; + private long? _idleSince; + private bool _disposed; + + private readonly Timer? _renewTimer; + private readonly SlidingWindowRateLimiterOptions _options; + 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 + private object Lock => _queue; + + private static readonly RateLimitLease SuccessfulLease = new SlidingWindowLease(true, null); + private static readonly RateLimitLease FailedLease = new SlidingWindowLease(false, null); + private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency; + + /// + public override TimeSpan? IdleDuration => _idleSince is null ? null : new TimeSpan((long)((Stopwatch.GetTimestamp() - _idleSince) * TickFrequency)); + + /// + public override bool IsAutoReplenishing => _options.AutoReplenishment; + + /// + public override TimeSpan ReplenishmentPeriod => _options.Window; + + /// + /// Initializes the . + /// + /// Options to specify the behavior of the . + public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options!!) + { + _options = options; + _requestCount = options.PermitLimit; + _requestsPerSegment = new int[options.SegmentsPerWindow]; + TimeSpan _windowSegmentInterval = _options.Window / _options.SegmentsPerWindow; + _currentSegmentIndex = 0; + + if (_options.AutoReplenishment) + { + _renewTimer = new Timer(Replenish, this, _windowSegmentInterval, _windowSegmentInterval); + } + } + + /// + public override int GetAvailablePermits() => _options.PermitLimit - _requestCount; + + /// + protected override RateLimitLease AcquireCore(int requestCount) + { + // These amounts of resources can never be acquired + if (requestCount > _options.PermitLimit) + { + throw new ArgumentOutOfRangeException(nameof(requestCount), requestCount, SR.Format(SR.PermitLimitExceeded, requestCount, _options.PermitLimit)); + } + + // Return SuccessfulLease or FailedLease depending to indicate limiter state + if (requestCount == 0 && !_disposed) + { + if (_requestCount > 0) + { + return SuccessfulLease; + } + + return FailedLease; + } + + lock (Lock) + { + if (TryLeaseUnsynchronized(requestCount, out RateLimitLease? lease)) + { + return lease; + } + + return FailedLease; + } + } + + /// + protected override ValueTask WaitAsyncCore(int requestCount, CancellationToken cancellationToken = default) + { + // These amounts of resources can never be acquired + if (requestCount > _options.PermitLimit) + { + throw new ArgumentOutOfRangeException(nameof(requestCount), requestCount, SR.Format(SR.PermitLimitExceeded, requestCount, _options.PermitLimit)); + } + + ThrowIfDisposed(); + + // Return SuccessfulAcquisition if resources are available + if (requestCount == 0 && _requestCount > 0) + { + return new ValueTask(SuccessfulLease); + } + + lock (Lock) + { + if (TryLeaseUnsynchronized(requestCount, out RateLimitLease? lease)) + { + return new ValueTask(lease); + } + + // Avoid integer overflow by using subtraction instead of addition + Debug.Assert(_options.QueueLimit >= _queueCount); + if (_options.QueueLimit - _queueCount < requestCount) + { + if (_options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst && requestCount <= _options.QueueLimit) + { + // remove oldest items from queue until there is space for the newest acquisition request + do + { + RequestRegistration oldestRequest = _queue.DequeueHead(); + _queueCount -= oldestRequest.Count; + Debug.Assert(_queueCount >= 0); + oldestRequest.Tcs.TrySetResult(FailedLease); + } + while (_options.QueueLimit - _queueCount < requestCount); + } + else + { + // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst + return new ValueTask(FailedLease); + } + } + + CancelQueueState tcs = new CancelQueueState(requestCount, this, cancellationToken); + CancellationTokenRegistration ctr = default; + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.Register(static obj => + { + ((CancelQueueState)obj!).TrySetCanceled(); + }, tcs); + } + + RequestRegistration registration = new RequestRegistration(requestCount, tcs, ctr); + _queue.EnqueueTail(registration); + _queueCount += requestCount; + Debug.Assert(_queueCount <= _options.QueueLimit); + + return new ValueTask(registration.Tcs.Task); + } + } + + private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out RateLimitLease? lease) + { + ThrowIfDisposed(); + + // if requestCount is 0 we want to queue it if there are no available permits + if (_requestCount >= requestCount && _requestCount != 0) + { + if (requestCount == 0) + { + // Edge case where the check before the lock showed 0 available permits but when we got the lock some permits were now available + lease = SuccessfulLease; + return true; + } + + // a. if there are no items queued we can lease + // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest + if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst)) + { + _idleSince = null; + _requestsPerSegment[_currentSegmentIndex] += requestCount; + _requestCount -= requestCount; + Debug.Assert(_requestCount >= 0); + lease = SuccessfulLease; + return true; + } + } + + lease = null; + return false; + } + + /// + /// Attempts to replenish request counters in a window. + /// + /// + /// False if is enabled, otherwise true. + /// Does not reflect if permits were replenished. + /// + public override bool TryReplenish() + { + if (_options.AutoReplenishment) + { + return false; + } + Replenish(this); + return true; + } + + private static void Replenish(object? state) + { + SlidingWindowRateLimiter limiter = (state as SlidingWindowRateLimiter)!; + Debug.Assert(limiter is not null); + + // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change + long nowTicks = Stopwatch.GetTimestamp(); + limiter!.ReplenishInternal(nowTicks); + } + + // Used in tests that test behavior with specific time intervals + private void ReplenishInternal(long nowTicks) + { + TimeSpan _windowSegmentInterval = _options.Window / _options.SegmentsPerWindow; + + // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes + lock (Lock) + { + if (_disposed) + { + return; + } + + if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _windowSegmentInterval.Ticks) + { + return; + } + + _lastReplenishmentTick = nowTicks + _windowSegmentInterval.Ticks; + + _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; + int _oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; + _requestsPerSegment[_currentSegmentIndex] = 0; + + if (_oldSegmentRequestCount == 0) + { + return; + } + + // Process queued requests + _requestCount += _oldSegmentRequestCount; + Debug.Assert(_requestCount <= _options.PermitLimit); + while (_queue.Count > 0) + { + RequestRegistration nextPendingRequest = + _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.PeekHead() + : _queue.PeekTail(); + + if (_requestCount >= nextPendingRequest.Count) + { + // Request can be fulfilled + nextPendingRequest = + _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.DequeueHead() + : _queue.DequeueTail(); + + _queueCount -= nextPendingRequest.Count; + + _requestCount -= nextPendingRequest.Count; + _requestsPerSegment[_currentSegmentIndex] += _requestCount; + Debug.Assert(_requestCount >= 0); + + if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease)) + { + // Queued item was canceled so add count back + _requestCount += nextPendingRequest.Count; + _requestsPerSegment[_currentSegmentIndex] += _requestCount; + // Updating queue count is handled by the cancellation code + _queueCount += nextPendingRequest.Count; + } + nextPendingRequest.CancellationTokenRegistration.Dispose(); + Debug.Assert(_queueCount >= 0); + } + else + { + // Request cannot be fulfilled + break; + } + } + + if (_requestCount == _options.PermitLimit) + { + Debug.Assert(_idleSince is null); + Debug.Assert(_queueCount == 0); + _idleSince = Stopwatch.GetTimestamp(); + } + } + } + + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + lock (Lock) + { + if (_disposed) + { + return; + } + _disposed = true; + _renewTimer?.Dispose(); + while (_queue.Count > 0) + { + RequestRegistration next = _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.DequeueHead() + : _queue.DequeueTail(); + next.CancellationTokenRegistration.Dispose(); + next.Tcs.SetResult(FailedLease); + } + } + } + + protected override ValueTask DisposeAsyncCore() + { + Dispose(true); + + return default; + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SlidingWindowRateLimiter)); + } + } + + private sealed class SlidingWindowLease : RateLimitLease + { + private static readonly string[] s_allMetadataNames = new[] { MetadataName.RetryAfter.Name }; + + private readonly TimeSpan? _retryAfter; + + public SlidingWindowLease(bool isAcquired, TimeSpan? retryAfter) + { + IsAcquired = isAcquired; + _retryAfter = retryAfter; + } + + public override bool IsAcquired { get; } + + public override IEnumerable MetadataNames => s_allMetadataNames; + + public override bool TryGetMetadata(string metadataName, out object? metadata) + { + if (metadataName == MetadataName.RetryAfter.Name && _retryAfter.HasValue) + { + metadata = _retryAfter.Value; + return true; + } + + metadata = default; + return false; + } + } + + private readonly struct RequestRegistration + { + public RequestRegistration(int requestCount, TaskCompletionSource tcs, CancellationTokenRegistration cancellationTokenRegistration) + { + Count = requestCount; + // Use VoidAsyncOperationWithData instead + Tcs = tcs; + CancellationTokenRegistration = cancellationTokenRegistration; + } + + public int Count { get; } + + public TaskCompletionSource Tcs { get; } + + public CancellationTokenRegistration CancellationTokenRegistration { get; } + } + + private sealed class CancelQueueState : TaskCompletionSource + { + private readonly int _requestCount; + private readonly SlidingWindowRateLimiter _limiter; + private readonly CancellationToken _cancellationToken; + + public CancelQueueState(int requestCount, SlidingWindowRateLimiter limiter, CancellationToken cancellationToken) + : base(TaskCreationOptions.RunContinuationsAsynchronously) + { + _requestCount = requestCount; + _limiter = limiter; + _cancellationToken = cancellationToken; + } + + public new bool TrySetCanceled() + { + if (TrySetCanceled(_cancellationToken)) + { + lock (_limiter.Lock) + { + _limiter._queueCount -= _requestCount; + } + return true; + } + return false; + } + } + } +} 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 new file mode 100644 index 00000000000000..37231e1bc4fc5d --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.RateLimiting +{ + /// + /// Options to specify the behavior of a . + /// + public sealed class SlidingWindowRateLimiterOptions + { + /// + /// Initializes the . + /// + /// Maximum number of request counters that can be served in a window. + /// + /// Maximum number of unprocessed request counters waiting via . + /// + /// Specifies how often requests can be replenished. Replenishing is triggered either by an internal timer if is true, or by calling . + /// + /// Specified how many segments a window can be divided into. The total requests a segment can serve cannot exceed the max limit.. + /// + /// Specifies whether request replenishment will be handled by the or by another party via . + /// + /// When , , or are less than 0 + /// or when is more than 49 days. + public SlidingWindowRateLimiterOptions( + int permitLimit, + QueueProcessingOrder queueProcessingOrder, + int queueLimit, + TimeSpan window, + int segmentsPerWindow, + bool autoReplenishment = true) + { + if (permitLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(permitLimit)); + } + if (queueLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(queueLimit)); + } + if (segmentsPerWindow <= 0) + { + throw new ArgumentOutOfRangeException(nameof(segmentsPerWindow)); + } + + PermitLimit = permitLimit; + QueueProcessingOrder = queueProcessingOrder; + QueueLimit = queueLimit; + Window = window; + SegmentsPerWindow = segmentsPerWindow; + AutoReplenishment = autoReplenishment; + } + + /// + /// Specifies the minimum period between replenishments. + /// + public TimeSpan Window { get; } + + /// + /// Specifies the maximum number of segments a window is divided into. + /// + public int SegmentsPerWindow { get; } + + /// + /// Specified whether the is automatically replenishing request counters or if someone else + /// will be calling to replenish tokens. + /// + public bool AutoReplenishment { get; } + + /// + /// Maximum number of requests that can be served in a window. + /// + public int PermitLimit { get; } + + /// + /// Determines the behaviour of when not enough resources can be leased. + /// + /// + /// by default. + /// + public QueueProcessingOrder QueueProcessingOrder { get; } + + /// + /// Maximum cumulative permit count of queued acquisition requests. + /// + public int QueueLimit { get; } + } +} diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs new file mode 100644 index 00000000000000..143e3a7c70c8f1 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -0,0 +1,694 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Threading.RateLimiting.Test +{ + public class FixedWindowRateLimiterTests : BaseRateLimiterTests + { + [Fact] + public override void CanAcquireResource() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(); + + Assert.True(lease.IsAcquired); + Assert.False(limiter.Acquire().IsAcquired); + + lease.Dispose(); + Assert.False(limiter.Acquire().IsAcquired); + Assert.True(limiter.TryReplenish()); + + Assert.True(limiter.Acquire().IsAcquired); + } + + [Fact] + public override void InvalidOptionsThrows() + { + Assert.Throws( + () => new FixedWindowRateLimiterOptions(-1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromMinutes(2), autoReplenishment: false)); + Assert.Throws( + () => new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, -1, TimeSpan.FromMinutes(2), autoReplenishment: false)); + } + + [Fact] + public override async Task CanAcquireResourceAsync() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + + using var lease = await limiter.WaitAsync(); + + Assert.True(lease.IsAcquired); + var wait = limiter.WaitAsync(); + Assert.False(wait.IsCompleted); + + Assert.True(limiter.TryReplenish()); + + Assert.True((await wait).IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, autoReplenishment: false)); + var lease = await limiter.WaitAsync(); + + Assert.True(lease.IsAcquired); + var wait1 = limiter.WaitAsync(); + var wait2 = limiter.WaitAsync(); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + lease = await wait1; + Assert.True(lease.IsAcquired); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.True(limiter.TryReplenish()); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, + TimeSpan.FromMinutes(0), autoReplenishment: false)); + + var lease = await limiter.WaitAsync(2); + Assert.True(lease.IsAcquired); + + var wait1 = limiter.WaitAsync(2); + var wait2 = limiter.WaitAsync(); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + // second queued item completes first with NewestFirst + lease = await wait2; + Assert.True(lease.IsAcquired); + Assert.False(wait1.IsCompleted); + + lease.Dispose(); + Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); + + lease = await wait1; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + + var failedLease = await limiter.WaitAsync(1); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan)); + Assert.Equal(TimeSpan.Zero, timeSpan); + } + + [Fact] + public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(1); + var lease1 = await wait; + Assert.False(lease1.IsAcquired); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(2); + Assert.True(lease.IsAcquired); + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(1); + Assert.False(wait2.IsCompleted); + + var wait3 = limiter.WaitAsync(2); + var lease1 = await wait; + var lease2 = await wait2; + Assert.False(lease1.IsAcquired); + Assert.False(lease2.IsAcquired); + Assert.False(wait3.IsCompleted); + + limiter.TryReplenish(); + limiter.TryReplenish(); + + lease = await wait3; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimitAndNoAvailability_NewestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(2); + Assert.True(lease.IsAcquired); + + // Fill queue + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var lease1 = await limiter.WaitAsync(2); + Assert.False(lease1.IsAcquired); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + + var failedLease = await limiter.WaitAsync(1); + Assert.False(failedLease.IsAcquired); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + + wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(int.MaxValue, QueueProcessingOrder.NewestFirst, int.MaxValue, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(int.MaxValue); + Assert.True(lease.IsAcquired); + + // Fill queue + var wait = limiter.WaitAsync(3); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(int.MaxValue); + Assert.False(wait2.IsCompleted); + + var lease1 = await wait; + Assert.False(lease1.IsAcquired); + + limiter.TryReplenish(); + var lease2 = await wait2; + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override void ThrowsWhenAcquiringMoreThanLimit() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + Assert.Throws(() => limiter.Acquire(2)); + } + + [Fact] + public override async Task ThrowsWhenWaitingForMoreThanLimit() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + await Assert.ThrowsAsync(async () => await limiter.WaitAsync(2)); + } + + [Fact] + public override void ThrowsWhenAcquiringLessThanZero() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + Assert.Throws(() => limiter.Acquire(-1)); + } + + [Fact] + public override async Task ThrowsWhenWaitingForLessThanZero() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + await Assert.ThrowsAsync(async () => await limiter.WaitAsync(-1)); + } + + [Fact] + public override void AcquireZero_WithAvailability() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + + using var lease = limiter.Acquire(0); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void AcquireZero_WithoutAvailability() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + lease2.Dispose(); + } + + [Fact] + public override async Task WaitAsyncZero_WithAvailability() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + + using var lease = await limiter.WaitAsync(0); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(0); + Assert.False(wait.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + using var lease2 = await wait; + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override async Task CanDequeueMultipleResourcesAtOnce() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, autoReplenishment: false)); + using var lease = await limiter.WaitAsync(2); + Assert.True(lease.IsAcquired); + + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + var lease1 = await wait1; + var lease2 = await wait2; + Assert.True(lease1.IsAcquired); + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override async Task CanCancelWaitAsyncAfterQueuing() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + var wait = limiter.WaitAsync(1, cts.Token); + + cts.Cancel(); + var ex = await Assert.ThrowsAsync(() => wait.AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + Assert.Equal(1, limiter.GetAvailablePermits()); + } + + [Fact] + public override async Task CanCancelWaitAsyncBeforeQueuing() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = await Assert.ThrowsAsync(() => limiter.WaitAsync(1, cts.Token).AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + Assert.Equal(1, limiter.GetAvailablePermits()); + } + + [Fact] + public override async Task CancelUpdatesQueueLimit() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + var wait = limiter.WaitAsync(1, cts.Token); + + cts.Cancel(); + var ex = await Assert.ThrowsAsync(() => wait.AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void NoMetadataOnAcquiredLease() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.False(lease.TryGetMetadata(MetadataName.RetryAfter, out _)); + } + + [Fact] + public override void MetadataNamesContainsAllMetadata() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.Collection(lease.MetadataNames, metadataName => Assert.Equal(metadataName, MetadataName.RetryAfter.Name)); + } + + [Fact] + public override async Task DisposeReleasesQueuedAcquires() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + var wait3 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + Assert.False(wait3.IsCompleted); + + limiter.Dispose(); + + lease = await wait1; + Assert.False(lease.IsAcquired); + lease = await wait2; + Assert.False(lease.IsAcquired); + lease = await wait3; + Assert.False(lease.IsAcquired); + + // Throws after disposal + Assert.Throws(() => limiter.Acquire(1)); + await Assert.ThrowsAsync(() => limiter.WaitAsync(1).AsTask()); + } + + [Fact] + public override async Task DisposeAsyncReleasesQueuedAcquires() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + var wait3 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + Assert.False(wait3.IsCompleted); + + await limiter.DisposeAsync(); + + lease = await wait1; + Assert.False(lease.IsAcquired); + lease = await wait2; + Assert.False(lease.IsAcquired); + lease = await wait3; + Assert.False(lease.IsAcquired); + + // Throws after disposal + Assert.Throws(() => limiter.Acquire(1)); + await Assert.ThrowsAsync(() => limiter.WaitAsync(1).AsTask()); + } + + [Fact] + public async Task RetryMetadataOnFailedWaitAsync() + { + var options = new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), autoReplenishment: false); + var limiter = new FixedWindowRateLimiter(options); + + using var lease = limiter.Acquire(2); + + var failedLease = await limiter.WaitAsync(2); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata)); + var metaDataTime = Assert.IsType(metadata); + Assert.Equal(options.Window.Ticks * 2, metaDataTime.Ticks); + + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.RetryAfter.Name)); + } + + [Fact] + public async Task CorrectRetryMetadataWithQueuedItem() + { + var options = new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), autoReplenishment: false); + var limiter = new FixedWindowRateLimiter(options); + + using var lease = limiter.Acquire(2); + // Queue item which changes the retry after time for failed items + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var failedLease = await limiter.WaitAsync(2); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 3, typedMetadata.Ticks); + } + + + [Fact] + public async Task CorrectRetryMetadataWithNonZeroAvailableItems() + { + var options = new FixedWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), autoReplenishment: false); + var limiter = new FixedWindowRateLimiter(options); + + using var lease = limiter.Acquire(2); + + var failedLease = await limiter.WaitAsync(3); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + } + + [Fact] + public void TryReplenishWithAutoReplenish_ReturnsFalse() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(1), autoReplenishment: true)); + Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.False(limiter.TryReplenish()); + Assert.Equal(2, limiter.GetAvailablePermits()); + } + + [Fact] + public async Task AutoReplenish_ReplenishesCounters() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromMilliseconds(1000), autoReplenishment: true)); + Assert.Equal(2, limiter.GetAvailablePermits()); + limiter.Acquire(2); + + var lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, + TimeSpan.Zero, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + Assert.Equal(1, limiter.GetAvailablePermits()); + lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + var wait2 = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, + TimeSpan.Zero, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + lease = limiter.Acquire(1); + Assert.False(lease.IsAcquired); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void NullIdleDurationWhenActive() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.FromMilliseconds(2), autoReplenishment: false)); + limiter.Acquire(1); + Assert.Null(limiter.IdleDuration); + } + + [Fact] + public override async Task IdleDurationUpdatesWhenIdle() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.FromMilliseconds(2), autoReplenishment: false)); + Assert.NotNull(limiter.IdleDuration); + var previousDuration = limiter.IdleDuration; + await Task.Delay(15); + Assert.True(previousDuration < limiter.IdleDuration); + } + + [Fact] + public override void IdleDurationUpdatesWhenChangingFromActive() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, autoReplenishment: false)); + limiter.Acquire(1); + limiter.TryReplenish(); + Assert.NotNull(limiter.IdleDuration); + } + + [Fact] + public void ReplenishingRateLimiterPropertiesHaveCorrectValues() + { + var replenishPeriod = TimeSpan.FromMinutes(1); + using ReplenishingRateLimiter limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + replenishPeriod, autoReplenishment: true)); + Assert.True(limiter.IsAutoReplenishing); + Assert.Equal(replenishPeriod, limiter.Window); + + replenishPeriod = TimeSpan.FromSeconds(2); + using ReplenishingRateLimiter limiter2 = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + replenishPeriod, 1, autoReplenishment: false)); + Assert.False(limiter2.IsAutoReplenishing); + Assert.Equal(replenishPeriod, limiter2.Window); + } + } +} diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs new file mode 100644 index 00000000000000..172792a5ed57c7 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -0,0 +1,696 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Threading.RateLimiting.Test +{ + public class SlidingWindowRateLimiterTests : BaseRateLimiterTests + { + [Fact] + public override void CanAcquireResource() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(); + + Assert.True(lease.IsAcquired); + Assert.False(limiter.Acquire().IsAcquired); + + lease.Dispose(); + Assert.False(limiter.Acquire().IsAcquired); + Assert.True(limiter.TryReplenish()); + + Assert.True(limiter.Acquire().IsAcquired); + } + + [Fact] + public override void InvalidOptionsThrows() + { + Assert.Throws( + () => new SlidingWindowRateLimiterOptions(-1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromMinutes(2), 1, autoReplenishment: false)); + Assert.Throws( + () => new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, -1, TimeSpan.FromMinutes(2), 1, autoReplenishment: false)); + Assert.Throws( + () => new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromMinutes(2), -1, autoReplenishment: false)); + } + + [Fact] + public override async Task CanAcquireResourceAsync() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + + using var lease = await limiter.WaitAsync(); + + Assert.True(lease.IsAcquired); + var wait = limiter.WaitAsync(); + Assert.False(wait.IsCompleted); + + Assert.True(limiter.TryReplenish()); + + Assert.True((await wait).IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = await limiter.WaitAsync(); + + Assert.True(lease.IsAcquired); + var wait1 = limiter.WaitAsync(); + var wait2 = limiter.WaitAsync(); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + lease = await wait1; + Assert.True(lease.IsAcquired); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.True(limiter.TryReplenish()); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, + TimeSpan.FromMinutes(0), 1, autoReplenishment: false)); + + var lease = await limiter.WaitAsync(2); + Assert.True(lease.IsAcquired); + + var wait1 = limiter.WaitAsync(2); + var wait2 = limiter.WaitAsync(); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + // second queued item completes first with NewestFirst + lease = await wait2; + Assert.True(lease.IsAcquired); + Assert.False(wait1.IsCompleted); + + lease.Dispose(); + Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); + + lease = await wait1; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + + var failedLease = await limiter.WaitAsync(1); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan)); + Assert.Equal(TimeSpan.Zero, timeSpan); + } + + [Fact] + public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(1); + var lease1 = await wait; + Assert.False(lease1.IsAcquired); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(2); + Assert.True(lease.IsAcquired); + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(1); + Assert.False(wait2.IsCompleted); + + var wait3 = limiter.WaitAsync(2); + var lease1 = await wait; + var lease2 = await wait2; + Assert.False(lease1.IsAcquired); + Assert.False(lease2.IsAcquired); + Assert.False(wait3.IsCompleted); + + limiter.TryReplenish(); + limiter.TryReplenish(); + + lease = await wait3; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimitAndNoAvailability_NewestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(2); + Assert.True(lease.IsAcquired); + + // Fill queue + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var lease1 = await limiter.WaitAsync(2); + Assert.False(lease1.IsAcquired); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait = limiter.WaitAsync(1); + + var failedLease = await limiter.WaitAsync(1); + Assert.False(failedLease.IsAcquired); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + + wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(int.MaxValue, QueueProcessingOrder.NewestFirst, int.MaxValue, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(int.MaxValue); + Assert.True(lease.IsAcquired); + + // Fill queue + var wait = limiter.WaitAsync(3); + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(int.MaxValue); + Assert.False(wait2.IsCompleted); + + var lease1 = await wait; + Assert.False(lease1.IsAcquired); + + limiter.TryReplenish(); + var lease2 = await wait2; + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override void ThrowsWhenAcquiringMoreThanLimit() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + Assert.Throws(() => limiter.Acquire(2)); + } + + [Fact] + public override async Task ThrowsWhenWaitingForMoreThanLimit() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + await Assert.ThrowsAsync(async () => await limiter.WaitAsync(2)); + } + + [Fact] + public override void ThrowsWhenAcquiringLessThanZero() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + Assert.Throws(() => limiter.Acquire(-1)); + } + + [Fact] + public override async Task ThrowsWhenWaitingForLessThanZero() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + await Assert.ThrowsAsync(async () => await limiter.WaitAsync(-1)); + } + + [Fact] + public override void AcquireZero_WithAvailability() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + + using var lease = limiter.Acquire(0); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void AcquireZero_WithoutAvailability() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + lease2.Dispose(); + } + + [Fact] + public override async Task WaitAsyncZero_WithAvailability() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + + using var lease = await limiter.WaitAsync(0); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(0); + Assert.False(wait.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + using var lease2 = await wait; + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override async Task CanDequeueMultipleResourcesAtOnce() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, 1, autoReplenishment: false)); + using var lease = await limiter.WaitAsync(2); + Assert.True(lease.IsAcquired); + + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + var lease1 = await wait1; + var lease2 = await wait2; + Assert.True(lease1.IsAcquired); + Assert.True(lease2.IsAcquired); + } + + [Fact] + public override async Task CanCancelWaitAsyncAfterQueuing() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + var wait = limiter.WaitAsync(1, cts.Token); + + cts.Cancel(); + var ex = await Assert.ThrowsAsync(() => wait.AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + Assert.Equal(1, limiter.GetAvailablePermits()); + } + + [Fact] + public override async Task CanCancelWaitAsyncBeforeQueuing() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = await Assert.ThrowsAsync(() => limiter.WaitAsync(1, cts.Token).AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + lease.Dispose(); + Assert.True(limiter.TryReplenish()); + + Assert.Equal(1, limiter.GetAvailablePermits()); + } + + [Fact] + public override async Task CancelUpdatesQueueLimit() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 1, autoReplenishment: false)); + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var cts = new CancellationTokenSource(); + var wait = limiter.WaitAsync(1, cts.Token); + + cts.Cancel(); + var ex = await Assert.ThrowsAsync(() => wait.AsTask()); + Assert.Equal(cts.Token, ex.CancellationToken); + + wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void NoMetadataOnAcquiredLease() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 0, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.False(lease.TryGetMetadata(MetadataName.RetryAfter, out _)); + } + + [Fact] + public override void MetadataNamesContainsAllMetadata() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 0, autoReplenishment: false)); + using var lease = limiter.Acquire(1); + Assert.Collection(lease.MetadataNames, metadataName => Assert.Equal(metadataName, MetadataName.RetryAfter.Name)); + } + + [Fact] + public override async Task DisposeReleasesQueuedAcquires() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, 0, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + var wait3 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + Assert.False(wait3.IsCompleted); + + limiter.Dispose(); + + lease = await wait1; + Assert.False(lease.IsAcquired); + lease = await wait2; + Assert.False(lease.IsAcquired); + lease = await wait3; + Assert.False(lease.IsAcquired); + + // Throws after disposal + Assert.Throws(() => limiter.Acquire(1)); + await Assert.ThrowsAsync(() => limiter.WaitAsync(1).AsTask()); + } + + [Fact] + public override async Task DisposeAsyncReleasesQueuedAcquires() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, 0, autoReplenishment: false)); + var lease = limiter.Acquire(1); + var wait1 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(1); + var wait3 = limiter.WaitAsync(1); + Assert.False(wait1.IsCompleted); + Assert.False(wait2.IsCompleted); + Assert.False(wait3.IsCompleted); + + await limiter.DisposeAsync(); + + lease = await wait1; + Assert.False(lease.IsAcquired); + lease = await wait2; + Assert.False(lease.IsAcquired); + lease = await wait3; + Assert.False(lease.IsAcquired); + + // Throws after disposal + Assert.Throws(() => limiter.Acquire(1)); + await Assert.ThrowsAsync(() => limiter.WaitAsync(1).AsTask()); + } + + [Fact] + public async Task RetryMetadataOnFailedWaitAsync() + { + var options = new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), 1, autoReplenishment: false); + var limiter = new SlidingWindowRateLimiter(options); + + using var lease = limiter.Acquire(2); + + var failedLease = await limiter.WaitAsync(2); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata)); + var metaDataTime = Assert.IsType(metadata); + Assert.Equal(options.Window.Ticks * 2, metaDataTime.Ticks); + + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.RetryAfter.Name)); + } + + [Fact] + public async Task CorrectRetryMetadataWithQueuedItem() + { + var options = new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), 1, autoReplenishment: false); + var limiter = new SlidingWindowRateLimiterOptions(options); + + using var lease = limiter.Acquire(2); + // Queue item which changes the retry after time for failed items + var wait = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + + var failedLease = await limiter.WaitAsync(2); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 3, typedMetadata.Ticks); + } + + + [Fact] + public async Task CorrectRetryMetadataWithNonZeroAvailableItems() + { + var options = new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(20), 1, autoReplenishment: false); + var limiter = new SlidingWindowRateLimiter(options); + + using var lease = limiter.Acquire(2); + + var failedLease = await limiter.WaitAsync(3); + Assert.False(failedLease.IsAcquired); + Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); + Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + } + + [Fact] + public void TryReplenishWithAutoReplenish_ReturnsFalse() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromSeconds(1), 1, autoReplenishment: true)); + Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.False(limiter.TryReplenish()); + Assert.Equal(2, limiter.GetAvailablePermits()); + } + + [Fact] + public async Task AutoReplenish_ReplenishesCounters() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.FromMilliseconds(1000), 1, autoReplenishment: true)); + Assert.Equal(2, limiter.GetAvailablePermits()); + limiter.Acquire(2); + + var lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, + TimeSpan.Zero, 0, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + Assert.Equal(1, limiter.GetAvailablePermits()); + lease = await limiter.WaitAsync(1); + Assert.True(lease.IsAcquired); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, 0, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + var wait2 = limiter.WaitAsync(1); + Assert.False(wait.IsCompleted); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait2; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, + TimeSpan.Zero, 0, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.Zero, 0, autoReplenishment: false)); + + var lease = limiter.Acquire(1); + Assert.True(lease.IsAcquired); + + var wait = limiter.WaitAsync(2); + Assert.False(wait.IsCompleted); + + lease = limiter.Acquire(1); + Assert.False(lease.IsAcquired); + + limiter.TryReplenish(); + + lease = await wait; + Assert.True(lease.IsAcquired); + } + + [Fact] + public override void NullIdleDurationWhenActive() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.FromMilliseconds(2), 1, autoReplenishment: false)); + limiter.Acquire(1); + Assert.Null(limiter.IdleDuration); + } + + [Fact] + public override async Task IdleDurationUpdatesWhenIdle() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.FromMilliseconds(2), 1, autoReplenishment: false)); + Assert.NotNull(limiter.IdleDuration); + var previousDuration = limiter.IdleDuration; + await Task.Delay(15); + Assert.True(previousDuration < limiter.IdleDuration); + } + + [Fact] + public override void IdleDurationUpdatesWhenChangingFromActive() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.Zero, 0, autoReplenishment: false)); + limiter.Acquire(1); + limiter.TryReplenish(); + Assert.NotNull(limiter.IdleDuration); + } + + [Fact] + public void ReplenishingRateLimiterPropertiesHaveCorrectValues() + { + var replenishPeriod = TimeSpan.FromMinutes(1); + using ReplenishingRateLimiter limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + replenishPeriod, 1, autoReplenishment: true)); + Assert.True(limiter.IsAutoReplenishing); + Assert.Equal(replenishPeriod, limiter.Window); + + replenishPeriod = TimeSpan.FromSeconds(2); + using ReplenishingRateLimiter limiter2 = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + replenishPeriod, 1, autoReplenishment: false)); + Assert.False(limiter2.IsAutoReplenishing); + Assert.Equal(replenishPeriod, limiter2.Window); + } + } +} diff --git a/src/libraries/System.Threading.RateLimiting/tests/System.Threading.RateLimiting.Tests.csproj b/src/libraries/System.Threading.RateLimiting/tests/System.Threading.RateLimiting.Tests.csproj index 4bb1f2a3392643..0cef547da6ab17 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/System.Threading.RateLimiting.Tests.csproj +++ b/src/libraries/System.Threading.RateLimiting/tests/System.Threading.RateLimiting.Tests.csproj @@ -6,6 +6,8 @@ + + From ee146d5360db6bb9bf2d93723fae2934627710ad Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Mon, 18 Apr 2022 19:58:59 -0700 Subject: [PATCH 02/12] Add updates to the sliding window GetAvailablePermits --- .../RateLimiting/FixedWindowRateLimiter.cs | 4 +--- .../FixedWindowRateLimiterOptions.cs | 5 ++--- .../RateLimiting/SlidingWindowRateLimiter.cs | 21 ++++++++----------- .../SlidingWindowRateLimiterOptions.cs | 3 +-- 4 files changed, 13 insertions(+), 20 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 4bd4521cddf8c2..db4ad49f6354e2 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 @@ -38,13 +38,11 @@ public sealed class FixedWindowRateLimiter : ReplenishingRateLimiter /// public override TimeSpan ReplenishmentPeriod => _options.Window; - - /// /// Initializes the . /// /// Options to specify the behavior of the . - public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options!!) + public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options) { _options = options; _requestCount = options.PermitLimit; 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 ef6c035de84058..0b8693b479230b 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 @@ -15,13 +15,12 @@ public sealed class FixedWindowRateLimiterOptions /// /// Maximum number of unprocessed request counters waiting via . /// - /// Specifies how often request counters can be replenished. Replenishing is triggered either by an internal timer if is true, or by calling . + /// Specifies how often request counters can be replenished. Replenishing is triggered either by an internal timer if is true, or by calling . /// /// /// Specifies whether request replenishment will be handled by the or by another party via . /// - /// When or are less than 0 - /// or when is more than 49 days. + /// When or are less than 0. public FixedWindowRateLimiterOptions( int permitLimit, QueueProcessingOrder queueProcessingOrder, 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 2a4f48c2a66796..e8a4e457b5fca0 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 @@ -39,7 +39,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter public override bool IsAutoReplenishing => _options.AutoReplenishment; /// - public override TimeSpan ReplenishmentPeriod => _options.Window; + public override TimeSpan ReplenishmentPeriod => _options.Window / _options.SegmentsPerWindow; /// /// Initializes the . @@ -50,17 +50,16 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options!!) _options = options; _requestCount = options.PermitLimit; _requestsPerSegment = new int[options.SegmentsPerWindow]; - TimeSpan _windowSegmentInterval = _options.Window / _options.SegmentsPerWindow; _currentSegmentIndex = 0; if (_options.AutoReplenishment) { - _renewTimer = new Timer(Replenish, this, _windowSegmentInterval, _windowSegmentInterval); + _renewTimer = new Timer(Replenish, this, ReplenishmentPeriod, ReplenishmentPeriod); } } /// - public override int GetAvailablePermits() => _options.PermitLimit - _requestCount; + public override int GetAvailablePermits() => _requestCount; /// protected override RateLimitLease AcquireCore(int requestCount) @@ -220,8 +219,6 @@ private static void Replenish(object? state) // Used in tests that test behavior with specific time intervals private void ReplenishInternal(long nowTicks) { - TimeSpan _windowSegmentInterval = _options.Window / _options.SegmentsPerWindow; - // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes lock (Lock) { @@ -230,24 +227,24 @@ private void ReplenishInternal(long nowTicks) return; } - if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _windowSegmentInterval.Ticks) + if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < ReplenishmentPeriod.Ticks) { return; } - _lastReplenishmentTick = nowTicks + _windowSegmentInterval.Ticks; + _lastReplenishmentTick = nowTicks; _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; - int _oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; + int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; _requestsPerSegment[_currentSegmentIndex] = 0; - if (_oldSegmentRequestCount == 0) + if (oldSegmentRequestCount == 0) { return; } // Process queued requests - _requestCount += _oldSegmentRequestCount; + _requestCount += oldSegmentRequestCount; Debug.Assert(_requestCount <= _options.PermitLimit); while (_queue.Count > 0) { @@ -274,7 +271,7 @@ private void ReplenishInternal(long nowTicks) { // Queued item was canceled so add count back _requestCount += nextPendingRequest.Count; - _requestsPerSegment[_currentSegmentIndex] += _requestCount; + _requestsPerSegment[_currentSegmentIndex] -= _requestCount; // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } 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 37231e1bc4fc5d..159b4338f07d2b 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 @@ -21,8 +21,7 @@ public sealed class SlidingWindowRateLimiterOptions /// /// Specifies whether request replenishment will be handled by the or by another party via . /// - /// When , , or are less than 0 - /// or when is more than 49 days. + /// When , , or are less than 0. public SlidingWindowRateLimiterOptions( int permitLimit, QueueProcessingOrder queueProcessingOrder, From 62dc02ca1b82a5ce17f36e85971bef5b399eaabd Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 19 Apr 2022 10:16:06 -0700 Subject: [PATCH 03/12] Revert changes made to BclInterfaces/ref file --- .../System/Threading/RateLimiting/SlidingWindowRateLimiter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e8a4e457b5fca0..b3bee98663dbcb 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 @@ -39,7 +39,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter public override bool IsAutoReplenishing => _options.AutoReplenishment; /// - public override TimeSpan ReplenishmentPeriod => _options.Window / _options.SegmentsPerWindow; + public override TimeSpan ReplenishmentPeriod => new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow); /// /// Initializes the . From 43b5e0332e3b6989b89fd843ff27ad4cf2b2962f Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 19 Apr 2022 16:18:41 -0700 Subject: [PATCH 04/12] Update ref/System.Threading.RateLimiting --- .../ref/System.Threading.RateLimiting.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs index 7f1dbb49940a5d..5d83149cb8f118 100644 --- a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs +++ b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs @@ -119,16 +119,16 @@ public SlidingWindowRateLimiter(System.Threading.RateLimiting.SlidingWindowRateL public override System.TimeSpan? IdleDuration { get { throw null; } } public override bool IsAutoReplenishing { get { throw null; } } public override System.TimeSpan ReplenishmentPeriod { get { throw null; } } - protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount) { throw null; } + protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int requestCount) { throw null; } protected override void Dispose(bool disposing) { } protected override System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } public override int GetAvailablePermits() { throw null; } public override bool TryReplenish() { throw null; } - protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int requestCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class SlidingWindowRateLimiterOptions { - public SlidingWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, int segmentsPerWindow, bool autoRefresh = true) { } + public SlidingWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, int segmentsPerWindow, bool autoReplenishment = true) { } public bool AutoReplenishment { get { throw null; } } public int QueueLimit { get { throw null; } } public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } } @@ -142,16 +142,16 @@ public FixedWindowRateLimiter(System.Threading.RateLimiting.FixedWindowRateLimit public override System.TimeSpan? IdleDuration { get { throw null; } } public override bool IsAutoReplenishing { get { throw null; } } public override System.TimeSpan ReplenishmentPeriod { get { throw null; } } - protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount) { throw null; } + protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int requestCount) { throw null; } protected override void Dispose(bool disposing) { } protected override System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } public override int GetAvailablePermits() { throw null; } public override bool TryReplenish() { throw null; } - protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + protected override System.Threading.Tasks.ValueTask WaitAsyncCore(int requestCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class FixedWindowRateLimiterOptions { - public FixedWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, bool autoRefresh = true) { } + public FixedWindowRateLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan window, bool autoReplenishment = true) { } public bool AutoReplenishment { get { throw null; } } public int QueueLimit { get { throw null; } } public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } } From 1535b57190ee1d771c50f88de3c5f49477ac8e3a Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 19 Apr 2022 16:48:55 -0700 Subject: [PATCH 05/12] import Tasks namespace --- .../tests/FixedWindowRateLimiterTests.cs | 7 ++++--- .../tests/SlidingWindowRateLimiterTests.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index 143e3a7c70c8f1..566ae95ab5ae08 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Tasks; using Xunit; namespace System.Threading.RateLimiting.Test @@ -682,13 +683,13 @@ public void ReplenishingRateLimiterPropertiesHaveCorrectValues() using ReplenishingRateLimiter limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, replenishPeriod, autoReplenishment: true)); Assert.True(limiter.IsAutoReplenishing); - Assert.Equal(replenishPeriod, limiter.Window); + Assert.Equal(replenishPeriod, limiter.ReplenishmentPeriod); replenishPeriod = TimeSpan.FromSeconds(2); using ReplenishingRateLimiter limiter2 = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, - replenishPeriod, 1, autoReplenishment: false)); + replenishPeriod, autoReplenishment: false)); Assert.False(limiter2.IsAutoReplenishing); - Assert.Equal(replenishPeriod, limiter2.Window); + Assert.Equal(replenishPeriod, limiter2.ReplenishmentPeriod); } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index 172792a5ed57c7..f4351e18a3c6e0 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Tasks; using Xunit; namespace System.Threading.RateLimiting.Test @@ -504,7 +505,7 @@ public async Task CorrectRetryMetadataWithQueuedItem() { var options = new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, TimeSpan.FromSeconds(20), 1, autoReplenishment: false); - var limiter = new SlidingWindowRateLimiterOptions(options); + var limiter = new SlidingWindowRateLimiter(options); using var lease = limiter.Acquire(2); // Queue item which changes the retry after time for failed items @@ -684,13 +685,13 @@ public void ReplenishingRateLimiterPropertiesHaveCorrectValues() using ReplenishingRateLimiter limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, replenishPeriod, 1, autoReplenishment: true)); Assert.True(limiter.IsAutoReplenishing); - Assert.Equal(replenishPeriod, limiter.Window); + Assert.Equal(replenishPeriod, limiter.ReplenishmentPeriod); replenishPeriod = TimeSpan.FromSeconds(2); using ReplenishingRateLimiter limiter2 = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, replenishPeriod, 1, autoReplenishment: false)); Assert.False(limiter2.IsAutoReplenishing); - Assert.Equal(replenishPeriod, limiter2.Window); + Assert.Equal(replenishPeriod, limiter2.ReplenishmentPeriod); } } } From 7c89e545a2af3767d23df24d51b628295b6d0b68 Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Wed, 20 Apr 2022 14:10:53 -0700 Subject: [PATCH 06/12] Update tests anf fix up test failures caused by window segments --- .../tests/FixedWindowRateLimiterTests.cs | 11 ++++++----- .../tests/SlidingWindowRateLimiterTests.cs | 13 +++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index 566ae95ab5ae08..4c1bf9e1ac246f 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Threading.Tasks; using Xunit; @@ -102,7 +103,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() Assert.False(wait1.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); Assert.True(limiter.TryReplenish()); @@ -491,10 +492,10 @@ public async Task RetryMetadataOnFailedWaitAsync() Assert.False(failedLease.IsAcquired); Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata)); var metaDataTime = Assert.IsType(metadata); - Assert.Equal(options.Window.Ticks * 2, metaDataTime.Ticks); + Assert.Equal(options.Window.Ticks, metaDataTime.Ticks); Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + Assert.Equal(options.Window.Ticks, typedMetadata.Ticks); Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.RetryAfter.Name)); } @@ -513,7 +514,7 @@ public async Task CorrectRetryMetadataWithQueuedItem() var failedLease = await limiter.WaitAsync(2); Assert.False(failedLease.IsAcquired); Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 3, typedMetadata.Ticks); + Assert.Equal(options.Window.Ticks, typedMetadata.Ticks); } @@ -529,7 +530,7 @@ public async Task CorrectRetryMetadataWithNonZeroAvailableItems() var failedLease = await limiter.WaitAsync(3); Assert.False(failedLease.IsAcquired); Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); + Assert.Equal(options.Window.Ticks, typedMetadata.Ticks); } [Fact] diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index f4351e18a3c6e0..15b74c22b9202e 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Threading.Tasks; using Xunit; @@ -57,7 +58,7 @@ public override async Task CanAcquireResourceAsync() public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.FromMinutes(0), 1, autoReplenishment: false)); var lease = await limiter.WaitAsync(); Assert.True(lease.IsAcquired); @@ -412,7 +413,7 @@ public override async Task CancelUpdatesQueueLimit() public override void NoMetadataOnAcquiredLease() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); using var lease = limiter.Acquire(1); Assert.False(lease.TryGetMetadata(MetadataName.RetryAfter, out _)); } @@ -483,7 +484,7 @@ public override async Task DisposeAsyncReleasesQueuedAcquires() [Fact] public async Task RetryMetadataOnFailedWaitAsync() { - var options = new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + var options = new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1, TimeSpan.FromSeconds(20), 1, autoReplenishment: false); var limiter = new SlidingWindowRateLimiter(options); @@ -493,11 +494,7 @@ public async Task RetryMetadataOnFailedWaitAsync() Assert.False(failedLease.IsAcquired); Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata)); var metaDataTime = Assert.IsType(metadata); - Assert.Equal(options.Window.Ticks * 2, metaDataTime.Ticks); - - Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); - Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.RetryAfter.Name)); + Assert.Equal(options.Window.Ticks, metaDataTime.Ticks); } [Fact] From 4199046b6d39617e7bcf5588955040129943132d Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Mon, 25 Apr 2022 01:46:49 -0700 Subject: [PATCH 07/12] Add fixes to sliding window rate limiting tests --- .../RateLimiting/SlidingWindowRateLimiter.cs | 2 + .../tests/SlidingWindowRateLimiterTests.cs | 104 ++++-------------- 2 files changed, 26 insertions(+), 80 deletions(-) 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 b3bee98663dbcb..6d769ada5c357d 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 @@ -52,6 +52,8 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options!!) _requestsPerSegment = new int[options.SegmentsPerWindow]; _currentSegmentIndex = 0; + _idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp(); + if (_options.AutoReplenishment) { _renewTimer = new Timer(Replenish, this, ReplenishmentPeriod, ReplenishmentPeriod); diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index 15b74c22b9202e..e17a388b30e957 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -57,13 +57,13 @@ public override async Task CanAcquireResourceAsync() [Fact] public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(4, QueueProcessingOrder.OldestFirst, 5, TimeSpan.FromMinutes(0), 1, autoReplenishment: false)); - var lease = await limiter.WaitAsync(); + var lease = await limiter.WaitAsync(4); Assert.True(lease.IsAcquired); - var wait1 = limiter.WaitAsync(); - var wait2 = limiter.WaitAsync(); + var wait1 = limiter.WaitAsync(2); + var wait2 = limiter.WaitAsync(3); Assert.False(wait1.IsCompleted); Assert.False(wait2.IsCompleted); @@ -75,7 +75,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); lease = await wait2; @@ -105,7 +105,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() Assert.False(wait1.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); Assert.True(limiter.TryReplenish()); @@ -123,8 +123,6 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst() var failedLease = await limiter.WaitAsync(1); Assert.False(failedLease.IsAcquired); - Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan)); - Assert.Equal(TimeSpan.Zero, timeSpan); } [Fact] @@ -198,24 +196,20 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit [Fact] public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, TimeSpan.Zero, 1, autoReplenishment: false)); - var lease = limiter.Acquire(1); - var wait = limiter.WaitAsync(1); + var lease = limiter.Acquire(2); + var wait = limiter.WaitAsync(2); - var failedLease = await limiter.WaitAsync(1); + var failedLease = await limiter.WaitAsync(2); Assert.False(failedLease.IsAcquired); limiter.TryReplenish(); lease = await wait; Assert.True(lease.IsAcquired); - wait = limiter.WaitAsync(1); + wait = limiter.WaitAsync(2); Assert.False(wait.IsCompleted); - - limiter.TryReplenish(); - lease = await wait; - Assert.True(lease.IsAcquired); } [Fact] @@ -422,7 +416,7 @@ public override void NoMetadataOnAcquiredLease() public override void MetadataNamesContainsAllMetadata() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); using var lease = limiter.Acquire(1); Assert.Collection(lease.MetadataNames, metadataName => Assert.Equal(metadataName, MetadataName.RetryAfter.Name)); } @@ -431,7 +425,7 @@ public override void MetadataNamesContainsAllMetadata() public override async Task DisposeReleasesQueuedAcquires() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(1); var wait1 = limiter.WaitAsync(1); var wait2 = limiter.WaitAsync(1); @@ -458,7 +452,7 @@ public override async Task DisposeReleasesQueuedAcquires() public override async Task DisposeAsyncReleasesQueuedAcquires() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(1); var wait1 = limiter.WaitAsync(1); var wait2 = limiter.WaitAsync(1); @@ -481,56 +475,6 @@ public override async Task DisposeAsyncReleasesQueuedAcquires() await Assert.ThrowsAsync(() => limiter.WaitAsync(1).AsTask()); } - [Fact] - public async Task RetryMetadataOnFailedWaitAsync() - { - var options = new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.FromSeconds(20), 1, autoReplenishment: false); - var limiter = new SlidingWindowRateLimiter(options); - - using var lease = limiter.Acquire(2); - - var failedLease = await limiter.WaitAsync(2); - Assert.False(failedLease.IsAcquired); - Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata)); - var metaDataTime = Assert.IsType(metadata); - Assert.Equal(options.Window.Ticks, metaDataTime.Ticks); - } - - [Fact] - public async Task CorrectRetryMetadataWithQueuedItem() - { - var options = new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.FromSeconds(20), 1, autoReplenishment: false); - var limiter = new SlidingWindowRateLimiter(options); - - using var lease = limiter.Acquire(2); - // Queue item which changes the retry after time for failed items - var wait = limiter.WaitAsync(1); - Assert.False(wait.IsCompleted); - - var failedLease = await limiter.WaitAsync(2); - Assert.False(failedLease.IsAcquired); - Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 3, typedMetadata.Ticks); - } - - - [Fact] - public async Task CorrectRetryMetadataWithNonZeroAvailableItems() - { - var options = new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.FromSeconds(20), 1, autoReplenishment: false); - var limiter = new SlidingWindowRateLimiter(options); - - using var lease = limiter.Acquire(2); - - var failedLease = await limiter.WaitAsync(3); - Assert.False(failedLease.IsAcquired); - Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata)); - Assert.Equal(options.Window.Ticks * 2, typedMetadata.Ticks); - } - [Fact] public void TryReplenishWithAutoReplenish_ReturnsFalse() { @@ -557,7 +501,7 @@ public async Task AutoReplenish_ReplenishesCounters() public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -579,14 +523,14 @@ public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewe [Fact] public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 0, autoReplenishment: false)); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 5, + TimeSpan.Zero, 1, autoReplenishment: false)); - var lease = limiter.Acquire(1); + var lease = limiter.Acquire(3); Assert.True(lease.IsAcquired); var wait = limiter.WaitAsync(2); - var wait2 = limiter.WaitAsync(1); + var wait2 = limiter.WaitAsync(2); Assert.False(wait.IsCompleted); Assert.False(wait2.IsCompleted); @@ -606,7 +550,7 @@ public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfO public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -628,7 +572,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -657,8 +601,8 @@ public override void NullIdleDurationWhenActive() [Fact] public override async Task IdleDurationUpdatesWhenIdle() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.FromMilliseconds(2), 1, autoReplenishment: false)); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 2, + TimeSpan.FromMilliseconds(2), 2, autoReplenishment: false)); Assert.NotNull(limiter.IdleDuration); var previousDuration = limiter.IdleDuration; await Task.Delay(15); @@ -669,7 +613,7 @@ public override async Task IdleDurationUpdatesWhenIdle() public override void IdleDurationUpdatesWhenChangingFromActive() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.Zero, 0, autoReplenishment: false)); + TimeSpan.Zero, 1, autoReplenishment: false)); limiter.Acquire(1); limiter.TryReplenish(); Assert.NotNull(limiter.IdleDuration); From 5c00494ddb35dd20461493a942459c5f9e55b729 Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Mon, 25 Apr 2022 16:50:23 -0700 Subject: [PATCH 08/12] Update sliding window tests to use a window size greater than 1 --- .../RateLimiting/FixedWindowRateLimiter.cs | 8 +- .../RateLimiting/SlidingWindowRateLimiter.cs | 10 +-- .../tests/FixedWindowRateLimiterTests.cs | 2 - .../tests/SlidingWindowRateLimiterTests.cs | 75 +++++++++++-------- 4 files changed, 52 insertions(+), 43 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 db4ad49f6354e2..6ced5c9c018932 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 @@ -44,7 +44,7 @@ public sealed class FixedWindowRateLimiter : ReplenishingRateLimiter /// Options to specify the behavior of the . public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options) { - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); _requestCount = options.PermitLimit; _idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp(); @@ -72,7 +72,7 @@ protected override RateLimitLease AcquireCore(int requestCount) if (requestCount == 0 && !_disposed) { // Check if the requests are permitted in a window - // Requests will be allowed if the total served request is the max allowed requests (permit limit). + // Requests will be allowed if the total served request is less than the max allowed requests (permit limit). if (_requestCount > 0) { return SuccessfulLease; @@ -219,7 +219,7 @@ private static void Replenish(object? state) FixedWindowRateLimiter limiter = (state as FixedWindowRateLimiter)!; Debug.Assert(limiter is not null); - // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change + // Use Stopwatch instead of DateTime.UtcNow to avoid issues on systems where the clock can change long nowTicks = Stopwatch.GetTimestamp(); limiter!.ReplenishInternal(nowTicks); } @@ -261,7 +261,7 @@ private void ReplenishInternal(long nowTicks) Deque queue = _queue; _requestCount += resourcesToAdd; - Debug.Assert(_requestCount <= _options.PermitLimit); + Debug.Assert(_requestCount == _options.PermitLimit); while (queue.Count > 0) { RequestRegistration nextPendingRequest = 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 6d769ada5c357d..81390e331cecd2 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 @@ -45,9 +45,9 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter /// Initializes the . /// /// Options to specify the behavior of the . - public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options!!) + public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options) { - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); _requestCount = options.PermitLimit; _requestsPerSegment = new int[options.SegmentsPerWindow]; _currentSegmentIndex = 0; @@ -213,7 +213,7 @@ private static void Replenish(object? state) SlidingWindowRateLimiter limiter = (state as SlidingWindowRateLimiter)!; Debug.Assert(limiter is not null); - // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change + // Use Stopwatch instead of DateTime.UtcNow to avoid issues on systems where the clock can change long nowTicks = Stopwatch.GetTimestamp(); limiter!.ReplenishInternal(nowTicks); } @@ -236,8 +236,8 @@ private void ReplenishInternal(long nowTicks) _lastReplenishmentTick = nowTicks; - _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; + _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; _requestsPerSegment[_currentSegmentIndex] = 0; if (oldSegmentRequestCount == 0) @@ -272,7 +272,7 @@ private void ReplenishInternal(long nowTicks) if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease)) { // Queued item was canceled so add count back - _requestCount += nextPendingRequest.Count; + _requestCount -= nextPendingRequest.Count; _requestsPerSegment[_currentSegmentIndex] -= _requestCount; // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index 4c1bf9e1ac246f..f83b5c6a48b542 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -105,7 +105,6 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() lease.Dispose(); Assert.Equal(1, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); - Assert.True(limiter.TryReplenish()); lease = await wait1; Assert.True(lease.IsAcquired); @@ -165,7 +164,6 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir Assert.False(lease2.IsAcquired); Assert.False(wait3.IsCompleted); - limiter.TryReplenish(); limiter.TryReplenish(); lease = await wait3; diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index e17a388b30e957..ce10e0abefa2a7 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -13,7 +13,7 @@ public class SlidingWindowRateLimiterTests : BaseRateLimiterTests public override void CanAcquireResource() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(); Assert.True(lease.IsAcquired); @@ -41,7 +41,7 @@ public override void InvalidOptionsThrows() public override async Task CanAcquireResourceAsync() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 3, autoReplenishment: false)); using var lease = await limiter.WaitAsync(); @@ -52,18 +52,25 @@ public override async Task CanAcquireResourceAsync() Assert.True(limiter.TryReplenish()); Assert.True((await wait).IsAcquired); + + var wait2 = limiter.WaitAsync(); + Assert.False(wait2.IsCompleted); + + Assert.True(limiter.TryReplenish()); + + Assert.True((await wait).IsAcquired); } [Fact] public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(4, QueueProcessingOrder.OldestFirst, 5, - TimeSpan.FromMinutes(0), 1, autoReplenishment: false)); - var lease = await limiter.WaitAsync(4); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, + TimeSpan.FromMinutes(0), 2, autoReplenishment: false)); + var lease = await limiter.WaitAsync(2); Assert.True(lease.IsAcquired); - var wait1 = limiter.WaitAsync(2); - var wait2 = limiter.WaitAsync(3); + var wait1 = limiter.WaitAsync(); + var wait2 = limiter.WaitAsync(2); Assert.False(wait1.IsCompleted); Assert.False(wait2.IsCompleted); @@ -75,7 +82,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); lease = await wait2; @@ -86,7 +93,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, - TimeSpan.FromMinutes(0), 1, autoReplenishment: false)); + TimeSpan.FromMinutes(0), 2, autoReplenishment: false)); var lease = await limiter.WaitAsync(2); Assert.True(lease.IsAcquired); @@ -117,7 +124,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); using var lease = limiter.Acquire(1); var wait = limiter.WaitAsync(1); @@ -129,7 +136,7 @@ public override async Task FailsWhenQueuingMoreThanLimit_OldestFirst() public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(1); var wait = limiter.WaitAsync(1); Assert.False(wait.IsCompleted); @@ -149,7 +156,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst() public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); var wait = limiter.WaitAsync(1); @@ -176,7 +183,7 @@ public override async Task DropsMultipleOldestWhenQueuingMoreThanLimit_NewestFir public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimitAndNoAvailability_NewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); @@ -196,7 +203,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit [Fact] public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 2, TimeSpan.Zero, 1, autoReplenishment: false)); var lease = limiter.Acquire(2); var wait = limiter.WaitAsync(2); @@ -210,13 +217,17 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv wait = limiter.WaitAsync(2); Assert.False(wait.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; + Assert.True(lease.IsAcquired); } [Fact] public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(int.MaxValue, QueueProcessingOrder.NewestFirst, int.MaxValue, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(int.MaxValue); Assert.True(lease.IsAcquired); @@ -304,7 +315,7 @@ public override async Task WaitAsyncZero_WithAvailability() public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = await limiter.WaitAsync(1); Assert.True(lease.IsAcquired); @@ -321,7 +332,7 @@ public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability public override async Task CanDequeueMultipleResourcesAtOnce() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); using var lease = await limiter.WaitAsync(2); Assert.True(lease.IsAcquired); @@ -342,9 +353,9 @@ public override async Task CanDequeueMultipleResourcesAtOnce() [Fact] public override async Task CanCancelWaitAsyncAfterQueuing() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); - var lease = limiter.Acquire(1); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 2, autoReplenishment: false)); + var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); var cts = new CancellationTokenSource(); @@ -357,15 +368,15 @@ public override async Task CanCancelWaitAsyncAfterQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetAvailablePermits()); } [Fact] public override async Task CanCancelWaitAsyncBeforeQueuing() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); - var lease = limiter.Acquire(1); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 2, autoReplenishment: false)); + var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); var cts = new CancellationTokenSource(); @@ -377,15 +388,15 @@ public override async Task CanCancelWaitAsyncBeforeQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetAvailablePermits()); } [Fact] public override async Task CancelUpdatesQueueLimit() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); - var lease = limiter.Acquire(1); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + TimeSpan.Zero, 2, autoReplenishment: false)); + var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); var cts = new CancellationTokenSource(); @@ -395,7 +406,7 @@ public override async Task CancelUpdatesQueueLimit() var ex = await Assert.ThrowsAsync(() => wait.AsTask()); Assert.Equal(cts.Token, ex.CancellationToken); - wait = limiter.WaitAsync(1); + wait = limiter.WaitAsync(0); Assert.False(wait.IsCompleted); limiter.TryReplenish(); @@ -407,7 +418,7 @@ public override async Task CancelUpdatesQueueLimit() public override void NoMetadataOnAcquiredLease() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); using var lease = limiter.Acquire(1); Assert.False(lease.TryGetMetadata(MetadataName.RetryAfter, out _)); } @@ -489,7 +500,7 @@ public void TryReplenishWithAutoReplenish_ReturnsFalse() public async Task AutoReplenish_ReplenishesCounters() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, - TimeSpan.FromMilliseconds(1000), 1, autoReplenishment: true)); + TimeSpan.FromMilliseconds(1000), 2, autoReplenishment: true)); Assert.Equal(2, limiter.GetAvailablePermits()); limiter.Acquire(2); @@ -613,7 +624,7 @@ public override async Task IdleDurationUpdatesWhenIdle() public override void IdleDurationUpdatesWhenChangingFromActive() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); limiter.Acquire(1); limiter.TryReplenish(); Assert.NotNull(limiter.IdleDuration); From 64b9c2ebbc8c5574436602e7975afbfd58905508 Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 26 Apr 2022 11:56:15 -0700 Subject: [PATCH 09/12] Update how oldSegmentRequestCount is calculated --- .../RateLimiting/SlidingWindowRateLimiter.cs | 2 +- .../tests/SlidingWindowRateLimiterTests.cs | 67 ++++++++++++++----- 2 files changed, 51 insertions(+), 18 deletions(-) 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 81390e331cecd2..ce035d7ee5580b 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 @@ -236,8 +236,8 @@ private void ReplenishInternal(long nowTicks) _lastReplenishmentTick = nowTicks; - int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; + int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; _requestsPerSegment[_currentSegmentIndex] = 0; if (oldSegmentRequestCount == 0) diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index ce10e0abefa2a7..f6d2f59cbe4ef2 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -22,6 +22,7 @@ public override void CanAcquireResource() lease.Dispose(); Assert.False(limiter.Acquire().IsAcquired); Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); Assert.True(limiter.Acquire().IsAcquired); } @@ -40,25 +41,25 @@ public override void InvalidOptionsThrows() [Fact] public override async Task CanAcquireResourceAsync() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, - TimeSpan.Zero, 3, autoReplenishment: false)); + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 4, + TimeSpan.Zero, 2, autoReplenishment: false)); using var lease = await limiter.WaitAsync(); Assert.True(lease.IsAcquired); - var wait = limiter.WaitAsync(); + var wait = limiter.WaitAsync(2); Assert.False(wait.IsCompleted); Assert.True(limiter.TryReplenish()); - Assert.True((await wait).IsAcquired); + Assert.False(wait.IsCompleted); - var wait2 = limiter.WaitAsync(); + var wait2 = limiter.WaitAsync(2); Assert.False(wait2.IsCompleted); Assert.True(limiter.TryReplenish()); - Assert.True((await wait).IsAcquired); + Assert.True((await wait2).IsAcquired); } [Fact] @@ -77,6 +78,9 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() lease.Dispose(); Assert.True(limiter.TryReplenish()); + Assert.False(wait1.IsCompleted); + Assert.True(limiter.TryReplenish()); + lease = await wait1; Assert.True(lease.IsAcquired); Assert.False(wait2.IsCompleted); @@ -84,6 +88,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() lease.Dispose(); Assert.Equal(1, limiter.GetAvailablePermits()); Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); lease = await wait2; Assert.True(lease.IsAcquired); @@ -105,7 +110,9 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() lease.Dispose(); Assert.True(limiter.TryReplenish()); + Assert.False(wait2.IsCompleted); + Assert.True(limiter.TryReplenish()); // second queued item completes first with NewestFirst lease = await wait2; Assert.True(lease.IsAcquired); @@ -146,6 +153,7 @@ public override async Task DropsOldestWhenQueuingMoreThanLimit_NewestFirst() Assert.False(lease1.IsAcquired); Assert.False(wait2.IsCompleted); + limiter.TryReplenish(); limiter.TryReplenish(); lease = await wait2; @@ -194,6 +202,7 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit var lease1 = await limiter.WaitAsync(2); Assert.False(lease1.IsAcquired); + limiter.TryReplenish(); limiter.TryReplenish(); lease = await wait; @@ -204,13 +213,17 @@ public override async Task DropsRequestedLeaseIfPermitCountGreaterThanQueueLimit public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 3, autoReplenishment: false)); var lease = limiter.Acquire(2); var wait = limiter.WaitAsync(2); var failedLease = await limiter.WaitAsync(2); Assert.False(failedLease.IsAcquired); + limiter.TryReplenish(); + limiter.TryReplenish(); + Assert.False(wait.IsCompleted); + limiter.TryReplenish(); lease = await wait; Assert.True(lease.IsAcquired); @@ -219,6 +232,9 @@ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAv Assert.False(wait.IsCompleted); limiter.TryReplenish(); + limiter.TryReplenish(); + limiter.TryReplenish(); + lease = await wait; Assert.True(lease.IsAcquired); } @@ -241,6 +257,7 @@ public override async Task LargeAcquiresAndQueuesDoNotIntegerOverflow() var lease1 = await wait; Assert.False(lease1.IsAcquired); + limiter.TryReplenish(); limiter.TryReplenish(); var lease2 = await wait2; Assert.True(lease2.IsAcquired); @@ -324,6 +341,7 @@ public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability lease.Dispose(); Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); using var lease2 = await wait; Assert.True(lease2.IsAcquired); } @@ -331,7 +349,7 @@ public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability [Fact] public override async Task CanDequeueMultipleResourcesAtOnce() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2, + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 4, TimeSpan.Zero, 2, autoReplenishment: false)); using var lease = await limiter.WaitAsync(2); Assert.True(lease.IsAcquired); @@ -343,6 +361,7 @@ public override async Task CanDequeueMultipleResourcesAtOnce() lease.Dispose(); Assert.True(limiter.TryReplenish()); + Assert.True(limiter.TryReplenish()); var lease1 = await wait1; var lease2 = await wait2; @@ -388,13 +407,13 @@ public override async Task CanCancelWaitAsyncBeforeQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetAvailablePermits()); } [Fact] public override async Task CancelUpdatesQueueLimit() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1, + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 1, TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(2); Assert.True(lease.IsAcquired); @@ -406,10 +425,12 @@ public override async Task CancelUpdatesQueueLimit() var ex = await Assert.ThrowsAsync(() => wait.AsTask()); Assert.Equal(cts.Token, ex.CancellationToken); - wait = limiter.WaitAsync(0); + wait = limiter.WaitAsync(); Assert.False(wait.IsCompleted); limiter.TryReplenish(); + limiter.TryReplenish(); + lease = await wait; Assert.True(lease.IsAcquired); } @@ -463,7 +484,7 @@ public override async Task DisposeReleasesQueuedAcquires() public override async Task DisposeAsyncReleasesQueuedAcquires() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(1); var wait1 = limiter.WaitAsync(1); var wait2 = limiter.WaitAsync(1); @@ -512,7 +533,7 @@ public async Task AutoReplenish_ReplenishesCounters() public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 3, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -526,7 +547,11 @@ public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewe Assert.False(wait.IsCompleted); limiter.TryReplenish(); + Assert.True(limiter.TryReplenish()); + + Assert.False(wait.IsCompleted); + Assert.True(limiter.TryReplenish()); lease = await wait; Assert.True(lease.IsAcquired); } @@ -535,7 +560,7 @@ public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewe public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 5, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(3); Assert.True(lease.IsAcquired); @@ -547,10 +572,15 @@ public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfO limiter.TryReplenish(); + Assert.False(wait.IsCompleted); + Assert.False(wait2.IsCompleted); + + limiter.TryReplenish(); + lease = await wait; Assert.True(lease.IsAcquired); - Assert.False(wait2.IsCompleted); + limiter.TryReplenish(); limiter.TryReplenish(); lease = await wait2; @@ -561,7 +591,7 @@ public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfO public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -573,6 +603,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); + limiter.TryReplenish(); limiter.TryReplenish(); lease = await wait; @@ -583,7 +614,7 @@ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewest public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst() { var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3, - TimeSpan.Zero, 1, autoReplenishment: false)); + TimeSpan.Zero, 2, autoReplenishment: false)); var lease = limiter.Acquire(1); Assert.True(lease.IsAcquired); @@ -595,6 +626,7 @@ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOld Assert.False(lease.IsAcquired); limiter.TryReplenish(); + Assert.True(limiter.TryReplenish()); lease = await wait; Assert.True(lease.IsAcquired); @@ -627,6 +659,7 @@ public override void IdleDurationUpdatesWhenChangingFromActive() TimeSpan.Zero, 2, autoReplenishment: false)); limiter.Acquire(1); limiter.TryReplenish(); + limiter.TryReplenish(); Assert.NotNull(limiter.IdleDuration); } From 60302a3b4f04d2b79bf31ad17577f7bf427d97c6 Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 26 Apr 2022 12:40:50 -0700 Subject: [PATCH 10/12] Add test to verify multiple acquires --- .../RateLimiting/SlidingWindowRateLimiter.cs | 2 +- .../tests/SlidingWindowRateLimiterTests.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) 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 ce035d7ee5580b..3d2bcfc10009b1 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 @@ -266,7 +266,7 @@ private void ReplenishInternal(long nowTicks) _queueCount -= nextPendingRequest.Count; _requestCount -= nextPendingRequest.Count; - _requestsPerSegment[_currentSegmentIndex] += _requestCount; + _requestsPerSegment[_currentSegmentIndex] += nextPendingRequest.Count; Debug.Assert(_requestCount >= 0); if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease)) diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index f6d2f59cbe4ef2..9e84b95414de54 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -62,6 +62,37 @@ public override async Task CanAcquireResourceAsync() Assert.True((await wait2).IsAcquired); } + [Fact] + public async Task CanAcquireMulitpleRequestsAsync() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(4, QueueProcessingOrder.NewestFirst, 4, + TimeSpan.Zero, 3, autoReplenishment: false)); + + using var lease = await limiter.WaitAsync(2); + + Assert.True(lease.IsAcquired); + var wait = limiter.WaitAsync(3); + Assert.False(wait.IsCompleted); + + Assert.True(limiter.TryReplenish()); + + Assert.False(wait.IsCompleted); + + var wait2 = limiter.WaitAsync(2); + Assert.True(wait2.IsCompleted); + + Assert.True(limiter.TryReplenish()); + + var wait3 = limiter.WaitAsync(2); + Assert.False(wait3.IsCompleted); + + Assert.True(limiter.TryReplenish()); + Assert.True((await wait3).IsAcquired); + + Assert.False((await wait).IsAcquired); + Assert.Equal(0, limiter.GetAvailablePermits()); + } + [Fact] public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() { From 7ac2e15e6bddf988736e5f561fec17a5d4f2e81a Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Tue, 26 Apr 2022 14:18:02 -0700 Subject: [PATCH 11/12] Adding comments --- .../RateLimiting/FixedWindowRateLimiter.cs | 30 +++++++++---------- .../RateLimiting/SlidingWindowRateLimiter.cs | 22 +++++++++----- 2 files changed, 29 insertions(+), 23 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 6ced5c9c018932..e6fbff009b31d6 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 @@ -103,7 +103,7 @@ protected override ValueTask WaitAsyncCore(int requestCount, Can ThrowIfDisposed(); - // Return SuccessfulAcquisition if requestedCount is 0 and resources are available + // Return SuccessfulAcquisition if requestCount is 0 and resources are available if (requestCount == 0 && _requestCount > 0) { return new ValueTask(SuccessfulLease); @@ -181,8 +181,8 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra return true; } - // a. if there are no items queued we can lease - // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest + // a. If there are no items queued we can lease + // b. If there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst)) { _idleSince = null; @@ -227,7 +227,7 @@ private static void Replenish(object? state) // Used in tests that test behavior with specific time intervals private void ReplenishInternal(long nowTicks) { - // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes + // Method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes lock (Lock) { if (_disposed) @@ -243,8 +243,7 @@ private void ReplenishInternal(long nowTicks) _lastReplenishmentTick = nowTicks; int availableRequestCounters = _requestCount; - FixedWindowRateLimiterOptions options = _options; - int maxPermits = options.PermitLimit; + int maxPermits = _options.PermitLimit; int resourcesToAdd; if (availableRequestCounters < maxPermits) @@ -257,25 +256,24 @@ private void ReplenishInternal(long nowTicks) return; } - // Process queued requests - Deque queue = _queue; - _requestCount += resourcesToAdd; Debug.Assert(_requestCount == _options.PermitLimit); - while (queue.Count > 0) + + // Process queued requests + while (_queue.Count > 0) { RequestRegistration nextPendingRequest = - options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst - ? queue.PeekHead() - : queue.PeekTail(); + _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.PeekHead() + : _queue.PeekTail(); if (_requestCount >= nextPendingRequest.Count) { // Request can be fulfilled nextPendingRequest = - options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst - ? queue.DequeueHead() - : queue.DequeueTail(); + _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst + ? _queue.DequeueHead() + : _queue.DequeueTail(); _queueCount -= nextPendingRequest.Count; _requestCount -= nextPendingRequest.Count; 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 3d2bcfc10009b1..0367bf33f698ec 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 @@ -49,6 +49,8 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); _requestCount = options.PermitLimit; + + // _requestsPerSegment holds the no. of acquired requests in each window segment _requestsPerSegment = new int[options.SegmentsPerWindow]; _currentSegmentIndex = 0; @@ -90,6 +92,7 @@ protected override RateLimitLease AcquireCore(int requestCount) return lease; } + // TODO: Acquire additional metadata during a failed lease decision return FailedLease; } } @@ -124,7 +127,7 @@ protected override ValueTask WaitAsyncCore(int requestCount, Can { if (_options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst && requestCount <= _options.QueueLimit) { - // remove oldest items from queue until there is space for the newest acquisition request + // Remove oldest items from queue until there is space for the newest acquisition request do { RequestRegistration oldestRequest = _queue.DequeueHead(); @@ -174,8 +177,8 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra return true; } - // a. if there are no items queued we can lease - // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest + // a. If there are no items queued we can lease + // b. If there are items queued but the processing order is NewestFirst, then we can lease the incoming request since it is the newest if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst)) { _idleSince = null; @@ -204,6 +207,8 @@ public override bool TryReplenish() { return false; } + + // Replenish call will slide the window one segment at a time Replenish(this); return true; } @@ -221,7 +226,7 @@ private static void Replenish(object? state) // Used in tests that test behavior with specific time intervals private void ReplenishInternal(long nowTicks) { - // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes + // Method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes lock (Lock) { if (_disposed) @@ -236,6 +241,8 @@ private void ReplenishInternal(long nowTicks) _lastReplenishmentTick = nowTicks; + // Increament the current segment index while move the window + // We need to know the no. of requests that were acquired in a segment perviously to ensure that we don't acquire more than the permit limit. _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; _requestsPerSegment[_currentSegmentIndex] = 0; @@ -245,9 +252,10 @@ private void ReplenishInternal(long nowTicks) return; } - // Process queued requests _requestCount += oldSegmentRequestCount; Debug.Assert(_requestCount <= _options.PermitLimit); + + // Process queued requests while (_queue.Count > 0) { RequestRegistration nextPendingRequest = @@ -255,6 +263,7 @@ private void ReplenishInternal(long nowTicks) ? _queue.PeekHead() : _queue.PeekTail(); + // If we have enough permits after replenishing to serve the queued requests if (_requestCount >= nextPendingRequest.Count) { // Request can be fulfilled @@ -264,7 +273,6 @@ private void ReplenishInternal(long nowTicks) : _queue.DequeueTail(); _queueCount -= nextPendingRequest.Count; - _requestCount -= nextPendingRequest.Count; _requestsPerSegment[_currentSegmentIndex] += nextPendingRequest.Count; Debug.Assert(_requestCount >= 0); @@ -273,7 +281,7 @@ private void ReplenishInternal(long nowTicks) { // Queued item was canceled so add count back _requestCount -= nextPendingRequest.Count; - _requestsPerSegment[_currentSegmentIndex] -= _requestCount; + _requestsPerSegment[_currentSegmentIndex] -= nextPendingRequest.Count; // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } From 5a10be286109b5ac8c36f1d4e7e59be266acb9ad Mon Sep 17 00:00:00 2001 From: Shreya Verma Date: Wed, 27 Apr 2022 12:25:31 -0700 Subject: [PATCH 12/12] Add the failed queued requests back to available permits --- .../Threading/RateLimiting/SlidingWindowRateLimiter.cs | 6 +++--- .../tests/SlidingWindowRateLimiterTests.cs | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) 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 0367bf33f698ec..3f28de48e1bd95 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 @@ -241,8 +241,8 @@ private void ReplenishInternal(long nowTicks) _lastReplenishmentTick = nowTicks; - // Increament the current segment index while move the window - // We need to know the no. of requests that were acquired in a segment perviously to ensure that we don't acquire more than the permit limit. + // Increment the current segment index while move the window + // We need to know the no. of requests that were acquired in a segment previously to ensure that we don't acquire more than the permit limit. _currentSegmentIndex = (_currentSegmentIndex + 1) % _options.SegmentsPerWindow; int oldSegmentRequestCount = _requestsPerSegment[_currentSegmentIndex]; _requestsPerSegment[_currentSegmentIndex] = 0; @@ -280,7 +280,7 @@ private void ReplenishInternal(long nowTicks) if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease)) { // Queued item was canceled so add count back - _requestCount -= nextPendingRequest.Count; + _requestCount += nextPendingRequest.Count; _requestsPerSegment[_currentSegmentIndex] -= nextPendingRequest.Count; // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index 9e84b95414de54..b9d1c1caf6f9f4 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -63,8 +63,11 @@ public override async Task CanAcquireResourceAsync() } [Fact] - public async Task CanAcquireMulitpleRequestsAsync() + public async Task CanAcquireMultipleRequestsAsync() { + // This test verifies the following behavior + // 1. when we have available permits after replenish to serve the queued requests + // 2. when the oldest item from queue is remove to accomodate new requests (QueueProcessingOrder: NewestFirst) var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(4, QueueProcessingOrder.NewestFirst, 4, TimeSpan.Zero, 3, autoReplenishment: false)); @@ -456,7 +459,7 @@ public override async Task CancelUpdatesQueueLimit() var ex = await Assert.ThrowsAsync(() => wait.AsTask()); Assert.Equal(cts.Token, ex.CancellationToken); - wait = limiter.WaitAsync(); + wait = limiter.WaitAsync(1); Assert.False(wait.IsCompleted); limiter.TryReplenish(); @@ -464,6 +467,7 @@ public override async Task CancelUpdatesQueueLimit() lease = await wait; Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetAvailablePermits()); } [Fact]