From e055449d89aefae252f95eca85e2790d8f5b51bc Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 12 Aug 2022 10:56:48 -0700 Subject: [PATCH] [RateLimiting] Add statistics API (#72306) --- ...ng.RateLimiting.Typeforwards.netcoreapp.cs | 6 + .../ref/System.Threading.RateLimiting.cs | 20 +- .../ref/System.Threading.RateLimiting.csproj | 6 +- .../src/System.Threading.RateLimiting.csproj | 7 + .../ChainedPartitionedRateLimiter.cs | 28 ++- .../RateLimiting/ConcurrencyLimiter.cs | 36 +++- .../DefaultPartitionedRateLimiter.cs | 4 +- .../RateLimiting/FixedWindowRateLimiter.cs | 30 ++- .../Threading/RateLimiting/NoopLimiter.cs | 26 ++- .../RateLimiting/PartitionedRateLimiter.T.cs | 14 +- .../RateLimiting/PartitionedRateLimiter.cs | 4 +- .../RateLimiting/RateLimitPartition.cs | 2 +- .../Threading/RateLimiting/RateLimiter.cs | 6 +- .../RateLimiting/RateLimiterStatistics.cs | 36 ++++ .../RateLimiting/SlidingWindowRateLimiter.cs | 30 ++- ...ng.RateLimiting.Typeforwards.netcoreapp.cs | 6 + .../RateLimiting/TokenBucketRateLimiter.cs | 30 ++- .../RateLimiting/TranslatingLimiter.cs | 4 +- .../tests/BaseRateLimiterTests.cs | 12 ++ .../tests/ChainedLimiterTests.cs | 199 ++++++++++++++---- .../tests/ConcurrencyLimiterTests.cs | 130 +++++++++++- .../tests/FixedWindowRateLimiterTests.cs | 150 ++++++++++++- .../tests/Infrastructure/Utils.cs | 22 +- .../tests/PartitionedRateLimiterTests.cs | 33 ++- .../tests/RateLimiterPartitionTests.cs | 60 +++++- .../tests/SlidingWindowRateLimiterTests.cs | 155 +++++++++++++- .../tests/TokenBucketRateLimiterTests.cs | 159 ++++++++++++-- 27 files changed, 1074 insertions(+), 141 deletions(-) create mode 100644 src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiterStatistics.cs create mode 100644 src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs diff --git a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs new file mode 100644 index 0000000000000..6a3b4117fcf50 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly +// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp System.Threading.RateLimiting +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] 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 1d48e8c717d12..b4905c286ecd9 100644 --- a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs +++ b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs @@ -14,7 +14,7 @@ public ConcurrencyLimiter(System.Threading.RateLimiting.ConcurrencyLimiterOption protected override System.Threading.RateLimiting.RateLimitLease AttemptAcquireCore(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 System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics() { throw null; } } public sealed partial class ConcurrencyLimiterOptions { @@ -33,7 +33,7 @@ public FixedWindowRateLimiter(System.Threading.RateLimiting.FixedWindowRateLimit protected override System.Threading.RateLimiting.RateLimitLease AttemptAcquireCore(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 System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics() { throw null; } public override bool TryReplenish() { throw null; } } public sealed partial class FixedWindowRateLimiterOptions @@ -78,7 +78,7 @@ public void Dispose() { } protected virtual void Dispose(bool disposing) { } public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } protected virtual System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } - public abstract int GetAvailablePermits(TResource resource); + public abstract System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics(TResource resource); public System.Threading.RateLimiting.PartitionedRateLimiter WithTranslatedKey(System.Func keyAdapter, bool leaveOpen) { throw null; } } public enum QueueProcessingOrder @@ -98,7 +98,15 @@ public void Dispose() { } protected virtual void Dispose(bool disposing) { } public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } protected virtual System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } - public abstract int GetAvailablePermits(); + public abstract System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics(); + } + public partial class RateLimiterStatistics + { + public RateLimiterStatistics() { } + public long CurrentAvailablePermits { get { throw null; } set { } } + public long CurrentQueuedCount { get { throw null; } set { } } + public long TotalFailedLeases { get { throw null; } set { } } + public long TotalSuccessfulLeases { get { throw null; } set { } } } public abstract partial class RateLimitLease : System.IDisposable { @@ -146,7 +154,7 @@ public SlidingWindowRateLimiter(System.Threading.RateLimiting.SlidingWindowRateL protected override System.Threading.RateLimiting.RateLimitLease AttemptAcquireCore(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 System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics() { throw null; } public override bool TryReplenish() { throw null; } } public sealed partial class SlidingWindowRateLimiterOptions @@ -169,7 +177,7 @@ public TokenBucketRateLimiter(System.Threading.RateLimiting.TokenBucketRateLimit protected override System.Threading.RateLimiting.RateLimitLease AttemptAcquireCore(int tokenCount) { 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 System.Threading.RateLimiting.RateLimiterStatistics? GetStatistics() { throw null; } public override bool TryReplenish() { throw null; } } public sealed partial class TokenBucketRateLimiterOptions diff --git a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.csproj b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.csproj index 8927acfb8ad41..12660a96ffbc6 100644 --- a/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.csproj +++ b/src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) @@ -7,7 +7,11 @@ + + + \ No newline at end of file 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 648d18fe85399..0d9f2ddcf5683 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj +++ b/src/libraries/System.Threading.RateLimiting/src/System.Threading.RateLimiting.csproj @@ -27,12 +27,14 @@ System.Threading.RateLimiting.RateLimitLease + + @@ -44,4 +46,9 @@ System.Threading.RateLimiting.RateLimitLease + + + + + diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ChainedPartitionedRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ChainedPartitionedRateLimiter.cs index 9a9a6114004e4..91ebe1126d9c9 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ChainedPartitionedRateLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ChainedPartitionedRateLimiter.cs @@ -23,21 +23,33 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter _limiters = limiters; } - public override int GetAvailablePermits(TResource resource) + public override RateLimiterStatistics? GetStatistics(TResource resource) { ThrowIfDisposed(); - int lowestPermitCount = int.MaxValue; + long lowestAvailablePermits = long.MaxValue; + long currentQueuedCount = 0; + long totalFailedLeases = 0; + long innerMostSuccessfulLeases = 0; foreach (PartitionedRateLimiter limiter in _limiters) { - int permitCount = limiter.GetAvailablePermits(resource); - - if (permitCount < lowestPermitCount) + if (limiter.GetStatistics(resource) is { } statistics) { - lowestPermitCount = permitCount; + if (statistics.CurrentAvailablePermits < lowestAvailablePermits) + { + lowestAvailablePermits = statistics.CurrentAvailablePermits; + } + currentQueuedCount += statistics.CurrentQueuedCount; + totalFailedLeases += statistics.TotalFailedLeases; + innerMostSuccessfulLeases = statistics.TotalSuccessfulLeases; } } - - return lowestPermitCount; + return new RateLimiterStatistics() + { + CurrentAvailablePermits = lowestAvailablePermits, + CurrentQueuedCount = currentQueuedCount, + TotalFailedLeases = totalFailedLeases, + TotalSuccessfulLeases = innerMostSuccessfulLeases, + }; } protected override RateLimitLease AttemptAcquireCore(TResource resource, int permitCount) diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs index b9dd45c20b87d..508340a2e138b 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs @@ -18,6 +18,9 @@ public sealed class ConcurrencyLimiter : RateLimiter private long? _idleSince = Stopwatch.GetTimestamp(); private bool _disposed; + private long _failedLeasesCount; + private long _successfulLeasesCount; + private readonly ConcurrencyLimiterOptions _options; private readonly Deque _queue = new Deque(); @@ -62,7 +65,17 @@ public ConcurrencyLimiter(ConcurrencyLimiterOptions options) } /// - public override int GetAvailablePermits() => _permitCount; + public override RateLimiterStatistics? GetStatistics() + { + ThrowIfDisposed(); + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _permitCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int permitCount) @@ -78,7 +91,13 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount) // Return SuccessfulLease or FailedLease to indicate limiter state if (permitCount == 0) { - return _permitCount > 0 ? SuccessfulLease : FailedLease; + if (_permitCount > 0) + { + Interlocked.Increment(ref _successfulLeasesCount); + return SuccessfulLease; + } + Interlocked.Increment(ref _failedLeasesCount); + return FailedLease; } // Perf: Check SemaphoreSlim implementation instead of locking @@ -93,6 +112,7 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount) } } + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } @@ -108,6 +128,7 @@ protected override ValueTask AcquireAsyncCore(int permitCount, C // Return SuccessfulLease if requestedCount is 0 and resources are available if (permitCount == 0 && _permitCount > 0 && !_disposed) { + Interlocked.Increment(ref _successfulLeasesCount); return new ValueTask(SuccessfulLease); } @@ -136,11 +157,16 @@ protected override ValueTask AcquireAsyncCore(int permitCount, C // Updating queue count is handled by the cancellation code _queueCount += oldestRequest.Count; } + else + { + Interlocked.Increment(ref _failedLeasesCount); + } } while (_options.QueueLimit - _queueCount < permitCount); } else { + Interlocked.Increment(ref _failedLeasesCount); // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst return new ValueTask(QueueLimitLease); } @@ -174,6 +200,7 @@ private bool TryLeaseUnsynchronized(int permitCount, [NotNullWhen(true)] out Rat { if (permitCount == 0) { + Interlocked.Increment(ref _successfulLeasesCount); // 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; @@ -186,6 +213,7 @@ private bool TryLeaseUnsynchronized(int permitCount, [NotNullWhen(true)] out Rat _idleSince = null; _permitCount -= permitCount; Debug.Assert(_permitCount >= 0); + Interlocked.Increment(ref _successfulLeasesCount); lease = new ConcurrencyLease(true, this, permitCount); return true; } @@ -234,6 +262,10 @@ private void Release(int releaseCount) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + Interlocked.Increment(ref _successfulLeasesCount); + } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); } diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/DefaultPartitionedRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/DefaultPartitionedRateLimiter.cs index b33bcbbd7b732..04e17168aef7a 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/DefaultPartitionedRateLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/DefaultPartitionedRateLimiter.cs @@ -65,9 +65,9 @@ private async Task RunTimer() _timer.Dispose(); } - public override int GetAvailablePermits(TResource resource) + public override RateLimiterStatistics? GetStatistics(TResource resource) { - return GetRateLimiter(resource).GetAvailablePermits(); + return GetRateLimiter(resource).GetStatistics(); } protected override RateLimitLease AttemptAcquireCore(TResource resource, int permitCount) 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 e72b86b9f0b25..fe4b0c29c3a62 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 @@ -19,6 +19,9 @@ public sealed class FixedWindowRateLimiter : ReplenishingRateLimiter private long? _idleSince; private bool _disposed; + private long _failedLeasesCount; + private long _successfulLeasesCount; + private readonly Timer? _renewTimer; private readonly FixedWindowRateLimiterOptions _options; private readonly Deque _queue = new Deque(); @@ -81,7 +84,17 @@ public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _requestCount; + public override RateLimiterStatistics? GetStatistics() + { + ThrowIfDisposed(); + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _requestCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int requestCount) @@ -100,9 +113,11 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) // Requests will be allowed if the total served request is less than the max allowed requests (permit limit). if (_requestCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedWindowLease(requestCount); } @@ -113,6 +128,7 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) return lease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedWindowLease(requestCount); } } @@ -131,6 +147,7 @@ protected override ValueTask AcquireAsyncCore(int requestCount, // Return SuccessfulAcquisition if requestCount is 0 and resources are available if (requestCount == 0 && _requestCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return new ValueTask(SuccessfulLease); } @@ -157,11 +174,16 @@ protected override ValueTask AcquireAsyncCore(int requestCount, { _queueCount += oldestRequest.Count; } + else + { + Interlocked.Increment(ref _failedLeasesCount); + } } while (_options.QueueLimit - _queueCount < requestCount); } else { + Interlocked.Increment(ref _failedLeasesCount); // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst return new ValueTask(CreateFailedWindowLease(requestCount)); } @@ -204,6 +226,7 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra { if (requestCount == 0) { + Interlocked.Increment(ref _successfulLeasesCount); // 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; @@ -216,6 +239,7 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra _idleSince = null; _requestCount -= requestCount; Debug.Assert(_requestCount >= 0); + Interlocked.Increment(ref _successfulLeasesCount); lease = SuccessfulLease; return true; } @@ -314,6 +338,10 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + Interlocked.Increment(ref _successfulLeasesCount); + } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); } diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/NoopLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/NoopLimiter.cs index 1744459ba2b75..0c6488145323e 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/NoopLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/NoopLimiter.cs @@ -10,18 +10,34 @@ internal sealed class NoopLimiter : RateLimiter { private static readonly RateLimitLease _lease = new NoopLease(); - private NoopLimiter() { } + private long _totalSuccessfulLeases; - public static NoopLimiter Instance { get; } = new NoopLimiter(); + public NoopLimiter() { } public override TimeSpan? IdleDuration => null; - public override int GetAvailablePermits() => int.MaxValue; + public override RateLimiterStatistics? GetStatistics() + { + return new RateLimiterStatistics() + { + CurrentAvailablePermits = long.MaxValue, + CurrentQueuedCount = 0, + TotalFailedLeases = 0, + TotalSuccessfulLeases = Interlocked.Read(ref _totalSuccessfulLeases) + }; + } - protected override RateLimitLease AttemptAcquireCore(int permitCount) => _lease; + protected override RateLimitLease AttemptAcquireCore(int permitCount) + { + Interlocked.Increment(ref _totalSuccessfulLeases); + return _lease; + } protected override ValueTask AcquireAsyncCore(int permitCount, CancellationToken cancellationToken) - => new ValueTask(_lease); + { + Interlocked.Increment(ref _totalSuccessfulLeases); + return new ValueTask(_lease); + } private sealed class NoopLease : RateLimitLease { diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.T.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.T.cs index fdc21eebeb61f..b97ac2de2c1cb 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.T.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.T.cs @@ -12,10 +12,10 @@ namespace System.Threading.RateLimiting public abstract class PartitionedRateLimiter : IAsyncDisposable, IDisposable { /// - /// An estimated count of available permits. + /// Gets a snapshot of the statistics for the if available. /// - /// - public abstract int GetAvailablePermits(TResource resource); + /// An instance of containing a snapshot of the statistics for a . + public abstract RateLimiterStatistics? GetStatistics(TResource resource); /// /// Fast synchronous attempt to acquire permits. @@ -126,16 +126,14 @@ public async ValueTask DisposeAsync() /// /// The type to translate into . /// The function to be called every time a is passed to - /// PartitionedRateLimiter<TOuter>.Acquire(TOuter, int) or PartitionedRateLimiter<TOuter>.WaitAsync(TOuter, int, CancellationToken). + /// PartitionedRateLimiter<TOuter>.Acquire(TOuter, int) or PartitionedRateLimiter<TOuter>.WaitAsync(TOuter, int, CancellationToken). + /// + /// should be implemented in a thread-safe way. /// Specifies whether the returned will dispose the wrapped . - /// or does not dispose the wrapped . /// A new PartitionedRateLimiter<TOuter> that translates /// to and calls the inner . public PartitionedRateLimiter WithTranslatedKey(Func keyAdapter, bool leaveOpen) { - // REVIEW: Do we want to have an option to dispose the inner limiter? - // Should the default be to dispose the inner limiter and have an option to not dispose it? - // See Stream wrappers like SslStream for prior-art return new TranslatingLimiter(this, keyAdapter, leaveOpen); } } diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.cs index aa79d5da4f0c8..a2f592c5ea3b5 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.cs @@ -38,7 +38,9 @@ public static PartitionedRateLimiter Create /// Methods on the returned will iterate over the passed in in the order given. /// /// - /// will return the lowest value of all the . + /// will return the lowest value for , + /// the inner-most limiter's , + /// and the aggregate values for the rest of the properties from the . /// /// /// s returned will aggregate metadata and for duplicates use the value of the first lease with the same metadata name. diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitPartition.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitPartition.cs index 9a98a8c5aff8f..23c33d2ad3bb6 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitPartition.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitPartition.cs @@ -48,7 +48,7 @@ public static RateLimitPartition GetConcurrencyLimiter( /// public static RateLimitPartition GetNoLimiter(TKey partitionKey) { - return Get(partitionKey, _ => NoopLimiter.Instance); + return Get(partitionKey, _ => new NoopLimiter()); } /// diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs index f44f85c1c2315..10dfca40dcb7a 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs @@ -11,10 +11,10 @@ namespace System.Threading.RateLimiting public abstract class RateLimiter : IAsyncDisposable, IDisposable { /// - /// An estimated count of available permits. + /// Gets a snapshot of the statistics if available. /// - /// - public abstract int GetAvailablePermits(); + /// An instance of containing a snapshot of the statistics. + public abstract RateLimiterStatistics? GetStatistics(); /// /// Specifies how long the has had all permits available. Used by RateLimiter managers that may want to diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiterStatistics.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiterStatistics.cs new file mode 100644 index 0000000000000..3853382a5f202 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiterStatistics.cs @@ -0,0 +1,36 @@ +// 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 +{ + /// + /// Snapshot of statistics for a . + /// + public class RateLimiterStatistics + { + /// + /// Initializes an instance of . + /// + public RateLimiterStatistics() { } + + /// + /// Gets the number of permits currently available for the . + /// + public long CurrentAvailablePermits { get; init; } + + /// + /// Gets the number of queued permits for the . + /// + public long CurrentQueuedCount { get; init; } + + /// + /// Gets the total number of failed s returned. + /// + public long TotalFailedLeases { get; init; } + + /// + /// Gets the total number of successful s returned. + /// + public long TotalSuccessfulLeases { get; init; } + } +} 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 a8b9b1fae3533..1ccf40775e2d8 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 @@ -21,6 +21,9 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter private long? _idleSince; private bool _disposed; + private long _failedLeasesCount; + private long _successfulLeasesCount; + private readonly Timer? _renewTimer; private readonly SlidingWindowRateLimiterOptions _options; private readonly Deque _queue = new Deque(); @@ -89,7 +92,17 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _requestCount; + public override RateLimiterStatistics? GetStatistics() + { + ThrowIfDisposed(); + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _requestCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int requestCount) @@ -105,9 +118,11 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) { if (_requestCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } @@ -119,6 +134,7 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) } // TODO: Acquire additional metadata during a failed lease decision + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } } @@ -137,6 +153,7 @@ protected override ValueTask AcquireAsyncCore(int requestCount, // Return SuccessfulAcquisition if resources are available if (requestCount == 0 && _requestCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return new ValueTask(SuccessfulLease); } @@ -163,11 +180,16 @@ protected override ValueTask AcquireAsyncCore(int requestCount, { _queueCount += oldestRequest.Count; } + else + { + Interlocked.Increment(ref _failedLeasesCount); + } } while (_options.QueueLimit - _queueCount < requestCount); } else { + Interlocked.Increment(ref _failedLeasesCount); // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst return new ValueTask(FailedLease); } @@ -201,6 +223,7 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra { if (requestCount == 0) { + Interlocked.Increment(ref _successfulLeasesCount); // 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; @@ -214,6 +237,7 @@ private bool TryLeaseUnsynchronized(int requestCount, [NotNullWhen(true)] out Ra _requestsPerSegment[_currentSegmentIndex] += requestCount; _requestCount -= requestCount; Debug.Assert(_requestCount >= 0); + Interlocked.Increment(ref _successfulLeasesCount); lease = SuccessfulLease; return true; } @@ -314,6 +338,10 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + Interlocked.Increment(ref _successfulLeasesCount); + } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); } diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs new file mode 100644 index 0000000000000..6a3b4117fcf50 --- /dev/null +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/System.Threading.RateLimiting.Typeforwards.netcoreapp.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly +// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp System.Threading.RateLimiting +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs index 90906e0235563..7baf91ea59080 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs @@ -19,6 +19,9 @@ public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter private long? _idleSince; private bool _disposed; + private long _failedLeasesCount; + private long _successfulLeasesCount; + private readonly Timer? _renewTimer; private readonly TokenBucketRateLimiterOptions _options; private readonly Deque _queue = new Deque(); @@ -83,7 +86,17 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _tokenCount; + public override RateLimiterStatistics? GetStatistics() + { + ThrowIfDisposed(); + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _tokenCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int tokenCount) @@ -99,9 +112,11 @@ protected override RateLimitLease AttemptAcquireCore(int tokenCount) { if (_tokenCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedTokenLease(tokenCount); } @@ -112,6 +127,7 @@ protected override RateLimitLease AttemptAcquireCore(int tokenCount) return lease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedTokenLease(tokenCount); } } @@ -130,6 +146,7 @@ protected override ValueTask AcquireAsyncCore(int tokenCount, Ca // Return SuccessfulAcquisition if requestedCount is 0 and resources are available if (tokenCount == 0 && _tokenCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return new ValueTask(SuccessfulLease); } @@ -157,11 +174,16 @@ protected override ValueTask AcquireAsyncCore(int tokenCount, Ca // Updating queue count is handled by the cancellation code _queueCount += oldestRequest.Count; } + else + { + Interlocked.Increment(ref _failedLeasesCount); + } } while (_options.QueueLimit - _queueCount < tokenCount); } else { + Interlocked.Increment(ref _failedLeasesCount); // Don't queue if queue limit reached and QueueProcessingOrder is OldestFirst return new ValueTask(CreateFailedTokenLease(tokenCount)); } @@ -206,6 +228,7 @@ private bool TryLeaseUnsynchronized(int tokenCount, [NotNullWhen(true)] out Rate { if (tokenCount == 0) { + Interlocked.Increment(ref _successfulLeasesCount); // 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; @@ -218,6 +241,7 @@ private bool TryLeaseUnsynchronized(int tokenCount, [NotNullWhen(true)] out Rate _idleSince = null; _tokenCount -= tokenCount; Debug.Assert(_tokenCount >= 0); + Interlocked.Increment(ref _successfulLeasesCount); lease = SuccessfulLease; return true; } @@ -318,6 +342,10 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + Interlocked.Increment(ref _successfulLeasesCount); + } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); } diff --git a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TranslatingLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TranslatingLimiter.cs index b5a59fe079bab..3c6748ab69603 100644 --- a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TranslatingLimiter.cs +++ b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TranslatingLimiter.cs @@ -20,11 +20,11 @@ public TranslatingLimiter(PartitionedRateLimiter inner, Func(() => chainedLimiter.GetAvailablePermits("")); + Assert.Throws(() => chainedLimiter.GetStatistics("")); Assert.Throws(() => chainedLimiter.AttemptAcquire("")); await Assert.ThrowsAsync(async () => await chainedLimiter.AcquireAsync("")); } @@ -84,13 +84,13 @@ public async Task DisposeAsyncMakesMethodsThrow() await chainedLimiter.DisposeAsync(); - Assert.Throws(() => chainedLimiter.GetAvailablePermits("")); + Assert.Throws(() => chainedLimiter.GetStatistics("")); Assert.Throws(() => chainedLimiter.AttemptAcquire("")); await Assert.ThrowsAsync(async () => await chainedLimiter.AcquireAsync("")); } [Fact] - public void AvailablePermitsReturnsLowestValue() + public void GetStatisticsReturnsLowestOrAggregateValues() { using var limiter1 = PartitionedRateLimiter.Create(resource => { @@ -99,7 +99,7 @@ public void AvailablePermitsReturnsLowestValue() { PermitLimit = 34, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, - QueueLimit = 0 + QueueLimit = 4 }); }); using var limiter2 = PartitionedRateLimiter.Create(resource => @@ -109,7 +109,7 @@ public void AvailablePermitsReturnsLowestValue() { PermitLimit = 22, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, - QueueLimit = 0 + QueueLimit = 2 }); }); using var limiter3 = PartitionedRateLimiter.Create(resource => @@ -119,16 +119,21 @@ public void AvailablePermitsReturnsLowestValue() { PermitLimit = 13, QueueProcessingOrder = QueueProcessingOrder.NewestFirst, - QueueLimit = 0 + QueueLimit = 10 }); }); using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); - Assert.Equal(13, chainedLimiter.GetAvailablePermits("")); + + var stats = chainedLimiter.GetStatistics(""); + Assert.Equal(13, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); } [Fact] - public void AvailablePermitsWithSingleLimiterWorks() + public void GetStatisticsWithSingleLimiterWorks() { using var limiter = PartitionedRateLimiter.Create(resource => { @@ -142,7 +147,121 @@ public void AvailablePermitsWithSingleLimiterWorks() }); using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter); - Assert.Equal(34, chainedLimiter.GetAvailablePermits("")); + + var stats = chainedLimiter.GetStatistics(""); + Assert.Equal(34, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + } + + [Fact] + public void GetStatisticsReturnsNewInstances() + { + using var limiter1 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 34, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 4 + }); + }); + using var limiter2 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 22, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 2 + }); + }); + using var limiter3 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 13, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 10 + }); + }); + + using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); + + var stats = chainedLimiter.GetStatistics(""); + var stats2 = chainedLimiter.GetStatistics(""); + Assert.NotSame(stats, stats2); + } + + [Fact] + public async Task GetStatisticsHasCorrectValues() + { + using var limiter1 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 34, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 4 + }); + }); + using var limiter2 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 22, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 2 + }); + }); + using var limiter3 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 13, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 10 + }); + }); + + using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); + + var lease = chainedLimiter.AttemptAcquire("", 10); + var stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(3, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalSuccessfulLeases); + Assert.Equal(0, stats.TotalFailedLeases); + + var lease2 = chainedLimiter.AttemptAcquire("", 10); + Assert.False(lease2.IsAcquired); + stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(3, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalFailedLeases); + + var task = chainedLimiter.AcquireAsync("", 10); + Assert.False(task.IsCompleted); + stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(2, stats.CurrentAvailablePermits); + Assert.Equal(10, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalFailedLeases); + + lease.Dispose(); + + lease = await task; + Assert.True(lease.IsAcquired); + stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(3, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalFailedLeases); } [Fact] @@ -257,12 +376,12 @@ public void AcquireLeaseCorrectlyDisposesWithMultipleLimiters() var lease = chainedLimiter.AttemptAcquire(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(0, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(0, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(1, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(1, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(1, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -293,12 +412,12 @@ public async Task AcquireAsyncLeaseCorrectlyDisposesWithMultipleLimiters() var lease = await chainedLimiter.AcquireAsync(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(0, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(0, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(1, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(1, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(1, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -319,10 +438,10 @@ public void AcquireLeaseCorrectlyDisposesWithSingleLimiter() var lease = chainedLimiter.AttemptAcquire(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -343,10 +462,10 @@ public async Task AcquireAsyncLeaseCorrectlyDisposesWithSingleLimiter() var lease = await chainedLimiter.AcquireAsync(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -442,7 +561,7 @@ public void AcquireFailsAndReleasesAcquiredResources() using var lease = chainedLimiter.AttemptAcquire(""); Assert.False(lease.IsAcquired); - Assert.Equal(1, concurrencyLimiter1.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -476,7 +595,7 @@ public async Task AcquireAsyncFailsAndReleasesAcquiredResources() using var lease = chainedLimiter.AttemptAcquire(""); Assert.False(lease.IsAcquired); - Assert.Equal(1, concurrencyLimiter1.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -499,7 +618,7 @@ public void AcquireThrowsAndReleasesAcquiredResources() using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2); Assert.Throws(() => chainedLimiter.AttemptAcquire("")); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -522,7 +641,7 @@ public async Task AcquireAsyncThrowsAndReleasesAcquiredResources() using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2); await Assert.ThrowsAsync(async () => await chainedLimiter.AcquireAsync("")); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -759,12 +878,12 @@ public void AcquireSucceedsDisposeThrowsAndReleasesResources() using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2); var lease = chainedLimiter.AttemptAcquire(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); var ex = Assert.Throws(() => lease.Dispose()); Assert.Single(ex.InnerExceptions); Assert.IsType(ex.InnerException); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -788,12 +907,12 @@ public async Task AcquireAsyncSucceedsDisposeThrowsAndReleasesResources() using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2); var lease = await chainedLimiter.AcquireAsync(""); Assert.True(lease.IsAcquired); - Assert.Equal(0, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); var ex = Assert.Throws(() => lease.Dispose()); Assert.Single(ex.InnerExceptions); Assert.IsType(ex.InnerException); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -823,12 +942,12 @@ public void AcquireForwardsCorrectPermitCount() var lease = chainedLimiter.AttemptAcquire("", 3); Assert.True(lease.IsAcquired); - Assert.Equal(2, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(0, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(2, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(0, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(5, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(3, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(5, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(3, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -858,12 +977,12 @@ public async Task AcquireAsyncForwardsCorrectPermitCount() var lease = await chainedLimiter.AcquireAsync("", 3); Assert.True(lease.IsAcquired); - Assert.Equal(2, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(0, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(2, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(0, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); lease.Dispose(); - Assert.Equal(5, concurrencyLimiter1.GetAvailablePermits()); - Assert.Equal(3, concurrencyLimiter2.GetAvailablePermits()); + Assert.Equal(5, concurrencyLimiter1.GetStatistics().CurrentAvailablePermits); + Assert.Equal(3, concurrencyLimiter2.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -974,15 +1093,15 @@ public async Task AcquireAsyncCanceledReleasesAcquiredResources() var lease = chainedLimiter.AttemptAcquire(""); Assert.True(lease.IsAcquired); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); var cts = new CancellationTokenSource(); var task = chainedLimiter.AcquireAsync("", 1, cts.Token); - Assert.Equal(0, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(0, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); cts.Cancel(); await Assert.ThrowsAsync(async () => await task); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] diff --git a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs index 45b22a28ae47e..54e2bbecc1975 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs @@ -536,7 +536,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing() lease.Dispose(); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics()?.CurrentAvailablePermits); } [Fact] @@ -559,7 +559,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing() lease.Dispose(); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics()?.CurrentAvailablePermits); } [Fact] @@ -804,5 +804,131 @@ public override void IdleDurationUpdatesWhenChangingFromActive() lease.Dispose(); Assert.NotNull(limiter.IdleDuration); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1 + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.AttemptAcquire(1); + + var stats2 = limiter.GetStatistics(); + Assert.NotSame(stats, stats2); + Assert.Equal(1, stats.CurrentAvailablePermits); + Assert.Equal(0, stats2.CurrentAvailablePermits); + } + + [Fact] + public override async Task GetStatisticsHasCorrectValues() + { + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50 + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(100, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + + // success from acquire + available + var lease1 = limiter.AttemptAcquire(60); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // queue + var lease2Task = limiter.AcquireAsync(50); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // failure from wait + var lease3 = await limiter.AcquireAsync(1); + Assert.False(lease3.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // failure from acquire + var lease4 = limiter.AttemptAcquire(100); + Assert.False(lease4.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + lease1.Dispose(); + await lease2Task; + + // success from wait + available + queue + stats = limiter.GetStatistics(); + Assert.Equal(50, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(2, stats.TotalSuccessfulLeases); + } + + [Fact] + public override async Task GetStatisticsWithZeroPermitCount() + { + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50 + }); + var lease = limiter.AttemptAcquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.AcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.AttemptAcquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.AttemptAcquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } + + [Fact] + public override void GetStatisticsThrowsAfterDispose() + { + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50 + }); + limiter.Dispose(); + Assert.Throws(limiter.GetStatistics); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index 7b521e6ba0442..6830a1ce74281 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -114,7 +114,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); lease = await wait2; @@ -150,7 +150,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() Assert.False(wait1.IsCompleted); lease.Dispose(); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); lease = await wait1; @@ -502,7 +502,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -528,7 +528,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -739,9 +739,9 @@ public void TryReplenishWithAutoReplenish_ReturnsFalse() Window = TimeSpan.FromSeconds(1), AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); Assert.False(limiter.TryReplenish()); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -755,7 +755,7 @@ public async Task AutoReplenish_ReplenishesCounters() Window = TimeSpan.FromMilliseconds(1000), AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); limiter.AttemptAcquire(2); var lease = await limiter.AcquireAsync(1); @@ -780,7 +780,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN var wait = limiter.AcquireAsync(2); Assert.False(wait.IsCompleted); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); lease = await limiter.AcquireAsync(1); Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); @@ -1017,5 +1017,139 @@ public override async Task CanDisposeAfterCancelingQueuedRequest() // Make sure dispose doesn't have any side-effects when dealing with a canceled queued item limiter.Dispose(); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1, + Window = TimeSpan.Zero, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.AttemptAcquire(1); + + var stats2 = limiter.GetStatistics(); + Assert.NotSame(stats, stats2); + Assert.Equal(1, stats.CurrentAvailablePermits); + Assert.Equal(0, stats2.CurrentAvailablePermits); + } + + [Fact] + public override async Task GetStatisticsHasCorrectValues() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(100, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + + // success from acquire + available + var lease1 = limiter.AttemptAcquire(60); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // queue + var lease2Task = limiter.AcquireAsync(50); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // failure from wait + var lease3 = await limiter.AcquireAsync(1); + Assert.False(lease3.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + // failure from acquire + var lease4 = limiter.AttemptAcquire(100); + Assert.False(lease4.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + limiter.TryReplenish(); + await lease2Task; + + // success from wait + available + queue + stats = limiter.GetStatistics(); + Assert.Equal(50, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(2, stats.TotalSuccessfulLeases); + } + + [Fact] + public override async Task GetStatisticsWithZeroPermitCount() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + AutoReplenishment = false + }); + var lease = limiter.AttemptAcquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.AcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.AttemptAcquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.AttemptAcquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } + + [Fact] + public override void GetStatisticsThrowsAfterDispose() + { + var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + AutoReplenishment = false + }); + limiter.Dispose(); + Assert.Throws(limiter.GetStatistics); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs b/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs index de62a2d6c065a..b506b7ee3e1dd 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs @@ -49,20 +49,20 @@ internal static Task RunTimerFunc(PartitionedRateLimiter limiter) internal sealed class NotImplementedPartitionedRateLimiter : PartitionedRateLimiter { - public override int GetAvailablePermits(T resource) => throw new NotImplementedException(); + public override RateLimiterStatistics? GetStatistics(T resource) => throw new NotImplementedException(); protected override RateLimitLease AttemptAcquireCore(T resource, int permitCount) => throw new NotImplementedException(); protected override ValueTask AcquireAsyncCore(T resource, int permitCount, CancellationToken cancellationToken) => throw new NotImplementedException(); } internal sealed class TrackingRateLimiter : RateLimiter { - private int _getAvailablePermitsCallCount; + private int _getStatisticsCallCount; private int _acquireCallCount; private int _waitAsyncCallCount; private int _disposeCallCount; private int _disposeAsyncCallCount; - public int GetAvailablePermitsCallCount => _getAvailablePermitsCallCount; + public int GetStatisticsCallCount => _getStatisticsCallCount; public int AcquireCallCount => _acquireCallCount; public int AcquireAsyncCallCount => _waitAsyncCallCount; public int DisposeCallCount => _disposeCallCount; @@ -70,10 +70,10 @@ internal sealed class TrackingRateLimiter : RateLimiter public override TimeSpan? IdleDuration => null; - public override int GetAvailablePermits() + public override RateLimiterStatistics? GetStatistics() { - Interlocked.Increment(ref _getAvailablePermitsCallCount); - return 1; + Interlocked.Increment(ref _getStatisticsCallCount); + return null; } protected override RateLimitLease AttemptAcquireCore(int permitCount) @@ -149,7 +149,7 @@ internal sealed class NotImplementedLimiter : RateLimiter { public override TimeSpan? IdleDuration => throw new NotImplementedException(); - public override int GetAvailablePermits() => throw new NotImplementedException(); + public override RateLimiterStatistics? GetStatistics() => throw new NotImplementedException(); protected override RateLimitLease AttemptAcquireCore(int permitCount) => throw new NotImplementedException(); protected override ValueTask AcquireAsyncCore(int permitCount, CancellationToken cancellationToken) => throw new NotImplementedException(); } @@ -159,8 +159,8 @@ internal sealed class CustomizableLimiter : RateLimiter public Func IdleDurationImpl { get; set; } = () => null; public override TimeSpan? IdleDuration => IdleDurationImpl(); - public Func GetAvailablePermitsImpl { get; set; } = () => throw new NotImplementedException(); - public override int GetAvailablePermits() => GetAvailablePermitsImpl(); + public Func GetStatisticsImpl{ get; set; } = () => throw new NotImplementedException(); + public override RateLimiterStatistics? GetStatistics() => GetStatisticsImpl(); public Func AttemptAcquireCoreImpl { get; set; } = _ => new Lease(); protected override RateLimitLease AttemptAcquireCore(int permitCount) => AttemptAcquireCoreImpl(permitCount); @@ -189,8 +189,8 @@ internal sealed class CustomizableReplenishingLimiter : ReplenishingRateLimiter public Func IdleDurationImpl { get; set; } = () => null; public override TimeSpan? IdleDuration => IdleDurationImpl(); - public Func GetAvailablePermitsImpl { get; set; } = () => throw new NotImplementedException(); - public override int GetAvailablePermits() => GetAvailablePermitsImpl(); + public Func GetStatisticsImpl { get; set; } = () => throw new NotImplementedException(); + public override RateLimiterStatistics? GetStatistics() => GetStatisticsImpl(); public Func AttemptAcquireCoreImpl { get; set; } = _ => new Lease(); protected override RateLimitLease AttemptAcquireCore(int permitCount) => AttemptAcquireCoreImpl(permitCount); diff --git a/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs index 4ba9b60ae75f2..b0380a0dcd6a4 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs @@ -61,7 +61,7 @@ public async Task Create_WaitAsyncCallsUnderlyingPartitionsLimiter() } [Fact] - public void Create_GetAvailablePermitsCallsUnderlyingPartitionsLimiter() + public void Create_GetStatisticsCallsUnderlyingPartitionsLimiter() { var limiterFactory = new TrackingRateLimiterFactory(); using var limiter = PartitionedRateLimiter.Create(resource => @@ -69,9 +69,9 @@ public void Create_GetAvailablePermitsCallsUnderlyingPartitionsLimiter() return RateLimitPartition.Get(1, key => limiterFactory.GetLimiter(key)); }); - limiter.GetAvailablePermits(""); + limiter.GetStatistics(""); Assert.Equal(1, limiterFactory.Limiters.Count); - Assert.Equal(1, limiterFactory.Limiters[0].Limiter.GetAvailablePermitsCallCount); + Assert.Equal(1, limiterFactory.Limiters[0].Limiter.GetStatisticsCallCount); } [Fact] @@ -766,13 +766,13 @@ public void Translate_GetAvailablePermitsPassesThroughToInnerLimiter() return i.ToString(); }, leaveOpen: true); - Assert.Equal(1, translateLimiter.GetAvailablePermits(1)); + Assert.Equal(1, translateLimiter.GetStatistics(1).CurrentAvailablePermits); Assert.Equal(1, translateCallCount); var lease = translateLimiter.AttemptAcquire(1); Assert.True(lease.IsAcquired); Assert.Equal(2, translateCallCount); - Assert.Equal(0, translateLimiter.GetAvailablePermits(1)); + Assert.Equal(0, translateLimiter.GetStatistics(1).CurrentAvailablePermits); Assert.Equal(3, translateCallCount); var lease2 = limiter.AttemptAcquire("1"); @@ -780,13 +780,13 @@ public void Translate_GetAvailablePermitsPassesThroughToInnerLimiter() lease.Dispose(); - Assert.Equal(1, translateLimiter.GetAvailablePermits(1)); + Assert.Equal(1, translateLimiter.GetStatistics(1).CurrentAvailablePermits); Assert.Equal(4, translateCallCount); lease = limiter.AttemptAcquire("1"); Assert.True(lease.IsAcquired); - Assert.Equal(0, translateLimiter.GetAvailablePermits(1)); + Assert.Equal(0, translateLimiter.GetStatistics(1).CurrentAvailablePermits); Assert.Equal(5, translateCallCount); } @@ -953,5 +953,24 @@ public async Task Translate_DisposeAsyncDoesDisposeInnerLimiter() Assert.Throws(() => limiter.AttemptAcquire("1")); Assert.Throws(() => translateLimiter.AttemptAcquire(1)); } + + [Fact] + public void Translate_GetStatisticsCallsUnderlyingLimiter() + { + var limiterFactory = new TrackingRateLimiterFactory(); + using var limiter = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.Get(1, key => limiterFactory.GetLimiter(key)); + }); + + var translateLimiter = limiter.WithTranslatedKey(i => + { + return i.ToString(); + }, leaveOpen: false); + + translateLimiter.GetStatistics(1); + Assert.Equal(1, limiterFactory.Limiters.Count); + Assert.Equal(1, limiterFactory.Limiters[0].Limiter.GetStatisticsCallCount); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs index 5676d1c16b96a..94fe202307ee9 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs @@ -21,7 +21,7 @@ public void Create_Concurrency() var limiter = partition.Factory(1); var concurrencyLimiter = Assert.IsType(limiter); - Assert.Equal(options.PermitLimit, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(options.PermitLimit, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -40,7 +40,7 @@ public void Create_TokenBucket() var limiter = partition.Factory(1); var tokenBucketLimiter = Assert.IsType(limiter); - Assert.Equal(options.TokenLimit, tokenBucketLimiter.GetAvailablePermits()); + Assert.Equal(options.TokenLimit, tokenBucketLimiter.GetStatistics().CurrentAvailablePermits); Assert.Equal(options.ReplenishmentPeriod, tokenBucketLimiter.ReplenishmentPeriod); Assert.False(tokenBucketLimiter.IsAutoReplenishing); } @@ -53,10 +53,10 @@ public async Task Create_NoLimiter() var limiter = partition.Factory(1); // How do we test an internal implementation of a limiter that doesn't limit? Just try some stuff that normal limiters would probably block on and see if it works. - var available = limiter.GetAvailablePermits(); + var available = limiter.GetStatistics().CurrentAvailablePermits; var lease = limiter.AttemptAcquire(int.MaxValue); Assert.True(lease.IsAcquired); - Assert.Equal(available, limiter.GetAvailablePermits()); + Assert.Equal(available, limiter.GetStatistics().CurrentAvailablePermits); lease = limiter.AttemptAcquire(int.MaxValue); Assert.True(lease.IsAcquired); @@ -69,6 +69,50 @@ public async Task Create_NoLimiter() lease.Dispose(); } + [Fact] + public async Task NoLimiter_GetStatistics() + { + var partition = RateLimitPartition.GetNoLimiter(1); + + var limiter = partition.Factory(1); + + var stats = limiter.GetStatistics(); + Assert.NotSame(stats, limiter.GetStatistics()); + Assert.Equal(long.MaxValue, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + + var leaseCount = 0; + for (var i = 0; i < 134; i++) + { + var lease = limiter.AttemptAcquire(i); + Assert.True(lease.IsAcquired); + ++leaseCount; + } + + stats = limiter.GetStatistics(); + Assert.Equal(long.MaxValue, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(leaseCount, stats.TotalSuccessfulLeases); + + for (var i = 0; i < 165; i++) + { + var wait = limiter.AcquireAsync(int.MaxValue); + Assert.True(wait.IsCompletedSuccessfully); + var lease = await wait; + Assert.True(lease.IsAcquired); + ++leaseCount; + } + + stats = limiter.GetStatistics(); + Assert.Equal(long.MaxValue, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(leaseCount, stats.TotalSuccessfulLeases); + } + [Fact] public void Create_AnyLimiter() { @@ -81,7 +125,7 @@ public void Create_AnyLimiter() var limiter = partition.Factory(1); var concurrencyLimiter = Assert.IsType(limiter); - Assert.Equal(1, concurrencyLimiter.GetAvailablePermits()); + Assert.Equal(1, concurrencyLimiter.GetStatistics().CurrentAvailablePermits); var partition2 = RateLimitPartition.Get(1, key => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions { @@ -94,7 +138,7 @@ public void Create_AnyLimiter() })); limiter = partition2.Factory(1); var tokenBucketLimiter = Assert.IsType(limiter); - Assert.Equal(1, tokenBucketLimiter.GetAvailablePermits()); + Assert.Equal(1, tokenBucketLimiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -112,7 +156,7 @@ public void Create_FixedWindow() var limiter = partition.Factory(1); var fixedWindowLimiter = Assert.IsType(limiter); - Assert.Equal(options.PermitLimit, fixedWindowLimiter.GetAvailablePermits()); + Assert.Equal(options.PermitLimit, fixedWindowLimiter.GetStatistics().CurrentAvailablePermits); Assert.Equal(options.Window, fixedWindowLimiter.ReplenishmentPeriod); Assert.False(fixedWindowLimiter.IsAutoReplenishing); } @@ -133,7 +177,7 @@ public void Create_SlidingWindow() var limiter = partition.Factory(1); var slidingWindowLimiter = Assert.IsType(limiter); - Assert.Equal(options.PermitLimit, slidingWindowLimiter.GetAvailablePermits()); + Assert.Equal(options.PermitLimit, slidingWindowLimiter.GetStatistics().CurrentAvailablePermits); Assert.Equal(TimeSpan.FromSeconds(11), slidingWindowLimiter.ReplenishmentPeriod); Assert.False(slidingWindowLimiter.IsAutoReplenishing); } diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index fc373ff2ec786..7241a39e0f1f4 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -148,7 +148,7 @@ public async Task CanAcquireMultipleRequestsAsync() Assert.True((await wait3).IsAcquired); Assert.False((await wait).IsAcquired); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -182,7 +182,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); Assert.True(limiter.TryReplenish()); @@ -222,7 +222,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() Assert.False(wait1.IsCompleted); lease.Dispose(); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); Assert.True(limiter.TryReplenish()); @@ -602,7 +602,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -629,7 +629,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -662,7 +662,7 @@ public override async Task CancelUpdatesQueueLimit() lease = await wait; Assert.True(lease.IsAcquired); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -777,9 +777,9 @@ public void TryReplenishWithAutoReplenish_ReturnsFalse() SegmentsPerWindow = 1, AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); Assert.False(limiter.TryReplenish()); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -794,7 +794,7 @@ public async Task AutoReplenish_ReplenishesCounters() SegmentsPerWindow = 2, AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); limiter.AttemptAcquire(2); var lease = await limiter.AcquireAsync(1); @@ -820,7 +820,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN var wait = limiter.AcquireAsync(2); Assert.False(wait.IsCompleted); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); lease = await limiter.AcquireAsync(1); Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); @@ -1080,5 +1080,140 @@ public override async Task CanDisposeAfterCancelingQueuedRequest() // Make sure dispose doesn't have any side-effects when dealing with a canceled queued item limiter.Dispose(); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1, + Window = TimeSpan.Zero, + SegmentsPerWindow = 2, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.AttemptAcquire(1); + + var stats2 = limiter.GetStatistics(); + Assert.NotSame(stats, stats2); + Assert.Equal(1, stats.CurrentAvailablePermits); + Assert.Equal(0, stats2.CurrentAvailablePermits); + } + + [Fact] + public override async Task GetStatisticsHasCorrectValues() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + SegmentsPerWindow = 2, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(100, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + + var lease1 = limiter.AttemptAcquire(60); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + var lease2Task = limiter.AcquireAsync(50); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + limiter.TryReplenish(); + + var lease3 = await limiter.AcquireAsync(1); + Assert.False(lease3.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + var lease4 = limiter.AttemptAcquire(100); + Assert.False(lease4.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + limiter.TryReplenish(); + await lease2Task; + + stats = limiter.GetStatistics(); + Assert.Equal(50, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(2, stats.TotalSuccessfulLeases); + } + + [Fact] + public override async Task GetStatisticsWithZeroPermitCount() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + SegmentsPerWindow = 3, + AutoReplenishment = false + }); + var lease = limiter.AttemptAcquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.AcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.AttemptAcquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.AttemptAcquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } + + [Fact] + public override void GetStatisticsThrowsAfterDispose() + { + var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + Window = TimeSpan.Zero, + SegmentsPerWindow = 3, + AutoReplenishment = false + }); + limiter.Dispose(); + Assert.Throws(limiter.GetStatistics); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs index 67e6624267fad..272c294a09b34 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs @@ -130,7 +130,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest() Assert.False(wait2.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); lease = await wait2; @@ -167,7 +167,7 @@ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest() Assert.False(wait1.IsCompleted); lease.Dispose(); - Assert.Equal(0, limiter.GetAvailablePermits()); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); Assert.True(limiter.TryReplenish()); @@ -537,7 +537,7 @@ public override async Task CanCancelAcquireAsyncAfterQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -630,7 +630,7 @@ public override async Task CanCancelAcquireAsyncBeforeQueuing() lease.Dispose(); Assert.True(limiter.TryReplenish()); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -902,12 +902,12 @@ public void TryReplenishHonorsTokensPerPeriod() Assert.True(limiter.AttemptAcquire(5).IsAcquired); Assert.False(limiter.AttemptAcquire(3).IsAcquired); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); - Assert.Equal(5, limiter.GetAvailablePermits()); + Assert.Equal(5, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); - Assert.Equal(7, limiter.GetAvailablePermits()); + Assert.Equal(7, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -922,9 +922,9 @@ public void TryReplenishWithAllTokensAvailable_Noops() TokensPerPeriod = 1, AutoReplenishment = false }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); Assert.True(limiter.TryReplenish()); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -939,9 +939,9 @@ public void TryReplenishWithAutoReplenish_ReturnsFalse() TokensPerPeriod = 1, AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); Assert.False(limiter.TryReplenish()); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); } [Fact] @@ -956,7 +956,7 @@ public async Task AutoReplenish_ReplenishesTokens() TokensPerPeriod = 1, AutoReplenishment = true }); - Assert.Equal(2, limiter.GetAvailablePermits()); + Assert.Equal(2, limiter.GetStatistics().CurrentAvailablePermits); limiter.AttemptAcquire(2); var lease = await limiter.AcquireAsync(1); @@ -982,7 +982,7 @@ public override async Task CanAcquireResourcesWithAcquireAsyncWithQueuedItemsIfN var wait = limiter.AcquireAsync(2); Assert.False(wait.IsCompleted); - Assert.Equal(1, limiter.GetAvailablePermits()); + Assert.Equal(1, limiter.GetStatistics().CurrentAvailablePermits); lease = await limiter.AcquireAsync(1); Assert.True(lease.IsAcquired); Assert.False(wait.IsCompleted); @@ -1205,5 +1205,138 @@ public void ReplenishingRateLimiterPropertiesHaveCorrectValues() Assert.False(limiter2.IsAutoReplenishing); Assert.Equal(replenishPeriod, limiter2.ReplenishmentPeriod); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1, + ReplenishmentPeriod = TimeSpan.Zero, + TokensPerPeriod = 2, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.AttemptAcquire(1); + + var stats2 = limiter.GetStatistics(); + Assert.NotSame(stats, stats2); + Assert.Equal(1, stats.CurrentAvailablePermits); + Assert.Equal(0, stats2.CurrentAvailablePermits); + } + + [Fact] + public override async Task GetStatisticsHasCorrectValues() + { + var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + ReplenishmentPeriod = TimeSpan.Zero, + TokensPerPeriod = 30, + AutoReplenishment = false + }); + + var stats = limiter.GetStatistics(); + Assert.Equal(100, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(0, stats.TotalSuccessfulLeases); + + var lease1 = limiter.AttemptAcquire(60); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + var lease2Task = limiter.AcquireAsync(50); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(0, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + var lease3 = await limiter.AcquireAsync(1); + Assert.False(lease3.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(1, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + var lease4 = limiter.AttemptAcquire(100); + Assert.False(lease4.IsAcquired); + stats = limiter.GetStatistics(); + Assert.Equal(40, stats.CurrentAvailablePermits); + Assert.Equal(50, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); + + limiter.TryReplenish(); + await lease2Task; + + stats = limiter.GetStatistics(); + Assert.Equal(20, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(2, stats.TotalFailedLeases); + Assert.Equal(2, stats.TotalSuccessfulLeases); + } + + [Fact] + public override async Task GetStatisticsWithZeroPermitCount() + { + var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + ReplenishmentPeriod = TimeSpan.Zero, + TokensPerPeriod = 30, + AutoReplenishment = false + }); + var lease = limiter.AttemptAcquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.AcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.AttemptAcquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.AttemptAcquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } + + [Fact] + public override void GetStatisticsThrowsAfterDispose() + { + var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50, + ReplenishmentPeriod = TimeSpan.Zero, + TokensPerPeriod = 30, + AutoReplenishment = false + }); + limiter.Dispose(); + Assert.Throws(limiter.GetStatistics); + } } }