From 4702a194c30c3ea95469611e9adc1d6cc59b70af Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Fri, 15 Jul 2022 18:17:27 -0700 Subject: [PATCH 1/4] [RateLimiting] Add statistics API --- ...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 | 31 +++- .../RateLimiting/ConcurrencyLimiter.cs | 34 +++- .../DefaultPartitionedRateLimiter.cs | 4 +- .../RateLimiting/FixedWindowRateLimiter.cs | 28 +++- .../Threading/RateLimiting/NoopLimiter.cs | 12 +- .../RateLimiting/PartitionedRateLimiter.T.cs | 6 +- .../RateLimiting/PartitionedRateLimiter.cs | 3 +- .../Threading/RateLimiting/RateLimiter.cs | 6 +- .../RateLimiting/RateLimiterStatistics.cs | 36 +++++ .../RateLimiting/SlidingWindowRateLimiter.cs | 28 +++- ...ng.RateLimiting.Typeforwards.netcoreapp.cs | 6 + .../RateLimiting/TokenBucketRateLimiter.cs | 28 +++- .../tests/BaseRateLimiterTests.cs | 6 + .../tests/ChainedLimiterTests.cs | 149 +++++++++++++----- .../tests/ConcurrencyLimiterTests.cs | 76 ++++++++- .../tests/FixedWindowRateLimiterTests.cs | 88 ++++++++++- .../tests/Infrastructure/Utils.cs | 22 +-- .../tests/PartitionedRateLimiterTests.cs | 6 +- .../tests/RateLimiterPartitionTests.cs | 16 +- .../tests/SlidingWindowRateLimiterTests.cs | 89 +++++++++-- .../tests/TokenBucketRateLimiterTests.cs | 93 +++++++++-- 25 files changed, 681 insertions(+), 125 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 00000000000000..6a3b4117fcf50b --- /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 1d48e8c717d126..b4905c286ecd9f 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 8927acfb8ad416..12660a96ffbc60 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 648d18fe85399e..0d9f2ddcf56832 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 9a9a6114004e42..15e3f204472e9c 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,36 @@ 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 lowestQueuedCount = long.MaxValue; + long totalFailedLeases = 0; + long totalSuccessfulLeases = 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; + } + if (statistics.CurrentQueuedCount < lowestQueuedCount) + { + lowestQueuedCount = statistics.CurrentQueuedCount; + } + totalFailedLeases += statistics.TotalFailedLeases; + totalSuccessfulLeases += statistics.TotalSuccessfulLeases; } } - - return lowestPermitCount; + return new RateLimiterStatistics() + { + CurrentAvailablePermits = lowestAvailablePermits, + CurrentQueuedCount = lowestQueuedCount, + TotalFailedLeases = totalFailedLeases, + TotalSuccessfulLeases = totalSuccessfulLeases, + }; } 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 b9dd45c20b87d9..9fdef4e1bb8d95 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,16 @@ public ConcurrencyLimiter(ConcurrencyLimiterOptions options) } /// - public override int GetAvailablePermits() => _permitCount; + public override RateLimiterStatistics? GetStatistics() + { + 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 +90,11 @@ 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) + { + return SuccessfulLease; + } + return FailedLease; } // Perf: Check SemaphoreSlim implementation instead of locking @@ -93,6 +109,7 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount) } } + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } @@ -136,11 +153,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); } @@ -186,6 +208,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 +257,13 @@ private void Release(int releaseCount) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + if (nextPendingRequest.Count != 0) + { + 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 b33bcbbd7b732b..04e17168aef7ae 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 e72b86b9f0b25f..bf4b3531437656 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,16 @@ public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _requestCount; + public override RateLimiterStatistics? GetStatistics() + { + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _requestCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int requestCount) @@ -113,6 +125,7 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) return lease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedWindowLease(requestCount); } } @@ -157,11 +170,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)); } @@ -216,6 +234,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 +333,13 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + if (nextPendingRequest.Count != 0) + { + 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 1744459ba2b752..e64ffe8f8008be 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 @@ -9,6 +9,13 @@ namespace System.Threading.RateLimiting internal sealed class NoopLimiter : RateLimiter { private static readonly RateLimitLease _lease = new NoopLease(); + private static readonly RateLimiterStatistics _stats = new RateLimiterStatistics() + { + CurrentAvailablePermits = long.MaxValue, + CurrentQueuedCount = 0, + TotalFailedLeases = 0, + TotalSuccessfulLeases = 0, + }; private NoopLimiter() { } @@ -16,7 +23,10 @@ private NoopLimiter() { } public override TimeSpan? IdleDuration => null; - public override int GetAvailablePermits() => int.MaxValue; + public override RateLimiterStatistics? GetStatistics() + { + return _stats; + } protected override RateLimitLease AttemptAcquireCore(int permitCount) => _lease; 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 fdc21eebeb61fb..04390b91c4c1e0 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. 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 aa79d5da4f0c8c..45f033758fac8a 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,8 @@ 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 values for + /// and 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/RateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs index f44f85c1c2315f..10dfca40dcb7a3 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 00000000000000..3853382a5f202c --- /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 a8b9b1fae35339..386dc99afce940 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,16 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _requestCount; + public override RateLimiterStatistics? GetStatistics() + { + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _requestCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int requestCount) @@ -119,6 +131,7 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) } // TODO: Acquire additional metadata during a failed lease decision + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } } @@ -163,11 +176,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); } @@ -214,6 +232,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 +333,13 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + if (nextPendingRequest.Count != 0) + { + 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 00000000000000..6a3b4117fcf50b --- /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 90906e02355636..c602f9fae08c0e 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,16 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options) } /// - public override int GetAvailablePermits() => _tokenCount; + public override RateLimiterStatistics? GetStatistics() + { + return new RateLimiterStatistics() + { + CurrentAvailablePermits = _tokenCount, + CurrentQueuedCount = _queueCount, + TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount), + TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount), + }; + } /// protected override RateLimitLease AttemptAcquireCore(int tokenCount) @@ -112,6 +124,7 @@ protected override RateLimitLease AttemptAcquireCore(int tokenCount) return lease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedTokenLease(tokenCount); } } @@ -157,11 +170,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)); } @@ -218,6 +236,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 +337,13 @@ private void ReplenishInternal(long nowTicks) // Updating queue count is handled by the cancellation code _queueCount += nextPendingRequest.Count; } + else + { + if (nextPendingRequest.Count != 0) + { + Interlocked.Increment(ref _successfulLeasesCount); + } + } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); } diff --git a/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs index 756d0a985ad014..e05f2597490d88 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs @@ -115,5 +115,11 @@ public abstract class BaseRateLimiterTests [Fact] public abstract void IdleDurationUpdatesWhenChangingFromActive(); + + [Fact] + public abstract void GetStatisticsReturnsNewInstances(); + + [Fact] + public abstract Task GetStatisticsHasCorrectValues(); } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs index ba1c179a5b121f..104476545f7679 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs @@ -52,7 +52,7 @@ public async Task DisposeMakesMethodsThrow() chainedLimiter.Dispose(); - Assert.Throws(() => 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,71 @@ 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(34, QueueProcessingOrder.NewestFirst, 4)); + }); + using var limiter2 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(22, QueueProcessingOrder.NewestFirst, 2)); + }); + using var limiter3 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(13, QueueProcessingOrder.NewestFirst, 10)); + }); + + using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); + + var stats = chainedLimiter.GetStatistics(""); + var stats2 = chainedLimiter.GetStatistics(""); + Assert.NotSame(stats, stats2); + } + + [Fact] + public void GetStatisticsHasCorrectValues() + { + using var limiter1 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(34, QueueProcessingOrder.NewestFirst, 4)); + }); + using var limiter2 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(22, QueueProcessingOrder.NewestFirst, 2)); + }); + using var limiter3 = PartitionedRateLimiter.Create(resource => + { + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(13, QueueProcessingOrder.NewestFirst, 10)); + }); + + using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); + + var lease = chainedLimiter.Acquire("", 10); + var stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(3, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(3, stats.TotalSuccessfulLeases); + Assert.Equal(0, stats.TotalFailedLeases); + + var lease2 = chainedLimiter.Acquire("", 10); + Assert.False(lease2.IsAcquired); + stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(3, stats.CurrentAvailablePermits); + Assert.Equal(0, stats.CurrentQueuedCount); + Assert.Equal(5, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalFailedLeases); } [Fact] @@ -257,12 +326,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 +362,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 +388,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 +412,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 +511,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 +545,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 +568,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 +591,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 +828,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 +857,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 +892,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 +927,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 +1043,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 45b22a28ae47e9..0263ec8fb2ccf5 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,77 @@ public override void IdleDurationUpdatesWhenChangingFromActive() lease.Dispose(); Assert.NotNull(limiter.IdleDuration); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1)); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.Acquire(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(100, QueueProcessingOrder.OldestFirst, 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.Acquire(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.WaitAndAcquireAsync(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.WaitAndAcquireAsync(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.Acquire(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); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index 7b521e6ba04424..f5b481366a7861 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,77 @@ 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(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, false)); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.Acquire(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(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, 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.Acquire(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.WaitAndAcquireAsync(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.WaitAndAcquireAsync(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.Acquire(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); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs b/src/libraries/System.Threading.RateLimiting/tests/Infrastructure/Utils.cs index de62a2d6c065a9..b506b7ee3e1dd4 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 4ba9b60ae75f2e..7ea96cd6656a9b 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] diff --git a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs index 5676d1c16b96af..d453401370184a 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); @@ -81,7 +81,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 +94,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 +112,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 +133,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 fc373ff2ec7863..322cbf6fa6ea61 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,74 @@ 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(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, 2, false)); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.Acquire(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(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, 2, 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.Acquire(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.WaitAndAcquireAsync(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.WaitAndAcquireAsync(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.Acquire(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); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs index 67e6624267fadb..ef8709b2c578db 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,72 @@ public void ReplenishingRateLimiterPropertiesHaveCorrectValues() Assert.False(limiter2.IsAutoReplenishing); Assert.Equal(replenishPeriod, limiter2.ReplenishmentPeriod); } + + [Fact] + public override void GetStatisticsReturnsNewInstances() + { + var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, 2, false)); + + var stats = limiter.GetStatistics(); + Assert.Equal(1, stats.CurrentAvailablePermits); + + var lease = limiter.Acquire(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(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, 30, 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.Acquire(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.WaitAndAcquireAsync(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.WaitAndAcquireAsync(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.Acquire(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); + } } } From df9d875a2e488d750745816d188159f6cc648258 Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Wed, 27 Jul 2022 16:45:53 -0700 Subject: [PATCH 2/4] fb --- .../ChainedPartitionedRateLimiter.cs | 9 ++-- .../RateLimiting/ConcurrencyLimiter.cs | 9 ++-- .../RateLimiting/FixedWindowRateLimiter.cs | 9 ++-- .../Threading/RateLimiting/NoopLimiter.cs | 30 ++++++----- .../RateLimiting/PartitionedRateLimiter.T.cs | 2 +- .../RateLimiting/PartitionedRateLimiter.cs | 4 +- .../RateLimiting/RateLimitPartition.cs | 2 +- .../RateLimiting/SlidingWindowRateLimiter.cs | 9 ++-- .../RateLimiting/TokenBucketRateLimiter.cs | 9 ++-- .../RateLimiting/TranslatingLimiter.cs | 4 +- .../tests/BaseRateLimiterTests.cs | 3 ++ .../tests/ChainedLimiterTests.cs | 51 +++++++++++++++--- .../tests/ConcurrencyLimiterTests.cs | 45 +++++++++++++++- .../tests/FixedWindowRateLimiterTests.cs | 51 +++++++++++++++++- .../tests/PartitionedRateLimiterTests.cs | 27 ++++++++-- .../tests/RateLimiterPartitionTests.cs | 44 +++++++++++++++ .../tests/SlidingWindowRateLimiterTests.cs | 54 ++++++++++++++++++- .../tests/TokenBucketRateLimiterTests.cs | 54 ++++++++++++++++++- 18 files changed, 358 insertions(+), 58 deletions(-) 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 15e3f204472e9c..f3047b941f9d59 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 @@ -27,7 +27,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter { ThrowIfDisposed(); long lowestAvailablePermits = long.MaxValue; - long lowestQueuedCount = long.MaxValue; + long currentQueuedCount = 0; long totalFailedLeases = 0; long totalSuccessfulLeases = 0; foreach (PartitionedRateLimiter limiter in _limiters) @@ -38,10 +38,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter { lowestAvailablePermits = statistics.CurrentAvailablePermits; } - if (statistics.CurrentQueuedCount < lowestQueuedCount) - { - lowestQueuedCount = statistics.CurrentQueuedCount; - } + currentQueuedCount += statistics.CurrentQueuedCount; totalFailedLeases += statistics.TotalFailedLeases; totalSuccessfulLeases += statistics.TotalSuccessfulLeases; } @@ -49,7 +46,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter return new RateLimiterStatistics() { CurrentAvailablePermits = lowestAvailablePermits, - CurrentQueuedCount = lowestQueuedCount, + CurrentQueuedCount = currentQueuedCount, TotalFailedLeases = totalFailedLeases, TotalSuccessfulLeases = totalSuccessfulLeases, }; 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 9fdef4e1bb8d95..0b0e5e8cfeaacb 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 @@ -92,8 +92,10 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount) { if (_permitCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } @@ -125,6 +127,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); } @@ -196,6 +199,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; @@ -259,10 +263,7 @@ private void Release(int releaseCount) } else { - if (nextPendingRequest.Count != 0) - { - Interlocked.Increment(ref _successfulLeasesCount); - } + Interlocked.Increment(ref _successfulLeasesCount); } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); 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 bf4b3531437656..651db82896ba5c 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 @@ -112,9 +112,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); } @@ -144,6 +146,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); } @@ -222,6 +225,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; @@ -335,10 +339,7 @@ private void ReplenishInternal(long nowTicks) } else { - if (nextPendingRequest.Count != 0) - { - Interlocked.Increment(ref _successfulLeasesCount); - } + 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 e64ffe8f8008be..0c6488145323e5 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 @@ -9,29 +9,35 @@ namespace System.Threading.RateLimiting internal sealed class NoopLimiter : RateLimiter { private static readonly RateLimitLease _lease = new NoopLease(); - private static readonly RateLimiterStatistics _stats = new RateLimiterStatistics() - { - CurrentAvailablePermits = long.MaxValue, - CurrentQueuedCount = 0, - TotalFailedLeases = 0, - TotalSuccessfulLeases = 0, - }; - private NoopLimiter() { } + private long _totalSuccessfulLeases; - public static NoopLimiter Instance { get; } = new NoopLimiter(); + public NoopLimiter() { } public override TimeSpan? IdleDuration => null; public override RateLimiterStatistics? GetStatistics() { - return _stats; + 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 04390b91c4c1e0..ee96362347b68a 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 @@ -127,8 +127,8 @@ 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). + /// 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) 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 45f033758fac8a..2bfe22e3f94966 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,8 +38,8 @@ public static PartitionedRateLimiter Create /// Methods on the returned will iterate over the passed in in the order given. /// /// - /// will return the lowest values for - /// and and the aggregate values for the rest of the properties from the . + /// will return the lowest value for + /// 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 9a98a8c5aff8f5..23c33d2ad3bb6f 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/SlidingWindowRateLimiter.cs b/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs index 386dc99afce940..6b3fd0955c3308 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 @@ -117,9 +117,11 @@ protected override RateLimitLease AttemptAcquireCore(int requestCount) { if (_requestCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return FailedLease; } @@ -150,6 +152,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); } @@ -219,6 +222,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; @@ -335,10 +339,7 @@ private void ReplenishInternal(long nowTicks) } else { - if (nextPendingRequest.Count != 0) - { - Interlocked.Increment(ref _successfulLeasesCount); - } + Interlocked.Increment(ref _successfulLeasesCount); } nextPendingRequest.CancellationTokenRegistration.Dispose(); Debug.Assert(_queueCount >= 0); 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 c602f9fae08c0e..11b043dde3aebb 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 @@ -111,9 +111,11 @@ protected override RateLimitLease AttemptAcquireCore(int tokenCount) { if (_tokenCount > 0) { + Interlocked.Increment(ref _successfulLeasesCount); return SuccessfulLease; } + Interlocked.Increment(ref _failedLeasesCount); return CreateFailedTokenLease(tokenCount); } @@ -143,6 +145,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); } @@ -224,6 +227,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; @@ -339,10 +343,7 @@ private void ReplenishInternal(long nowTicks) } else { - if (nextPendingRequest.Count != 0) - { - Interlocked.Increment(ref _successfulLeasesCount); - } + 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 b5a59fe079bab7..3c6748ab696035 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(resource => { - return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(34, QueueProcessingOrder.NewestFirst, 4)); + 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(22, QueueProcessingOrder.NewestFirst, 2)); + 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(13, QueueProcessingOrder.NewestFirst, 10)); + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 13, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 10 + }); }); using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); @@ -183,15 +198,30 @@ public void GetStatisticsHasCorrectValues() { using var limiter1 = PartitionedRateLimiter.Create(resource => { - return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions(34, QueueProcessingOrder.NewestFirst, 4)); + 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(22, QueueProcessingOrder.NewestFirst, 2)); + 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(13, QueueProcessingOrder.NewestFirst, 10)); + return RateLimitPartition.GetConcurrencyLimiter(1, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 13, + QueueProcessingOrder = QueueProcessingOrder.NewestFirst, + QueueLimit = 10 + }); }); using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); @@ -212,6 +242,15 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(0, stats.CurrentQueuedCount); Assert.Equal(5, stats.TotalSuccessfulLeases); Assert.Equal(1, stats.TotalFailedLeases); + + var task = chainedLimiter.WaitAndAcquireAsync("", 10); + Assert.False(task.IsCompleted); + stats = chainedLimiter.GetStatistics(""); + + Assert.Equal(2, stats.CurrentAvailablePermits); + Assert.Equal(10, stats.CurrentQueuedCount); + Assert.Equal(7, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalFailedLeases); } [Fact] diff --git a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs index 0263ec8fb2ccf5..6774b7e2b26f8b 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs @@ -808,7 +808,12 @@ public override void IdleDurationUpdatesWhenChangingFromActive() [Fact] public override void GetStatisticsReturnsNewInstances() { - var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1)); + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 1, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1 + }); var stats = limiter.GetStatistics(); Assert.Equal(1, stats.CurrentAvailablePermits); @@ -824,7 +829,12 @@ public override void GetStatisticsReturnsNewInstances() [Fact] public override async Task GetStatisticsHasCorrectValues() { - var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(100, QueueProcessingOrder.OldestFirst, 50)); + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 100, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 50 + }); var stats = limiter.GetStatistics(); Assert.Equal(100, stats.CurrentAvailablePermits); @@ -876,5 +886,36 @@ public override async Task GetStatisticsHasCorrectValues() 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.Acquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.WaitAndAcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.Acquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index f5b481366a7861..c032877e6809cf 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -1021,7 +1021,14 @@ public override async Task CanDisposeAfterCancelingQueuedRequest() [Fact] public override void GetStatisticsReturnsNewInstances() { - var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, false)); + 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); @@ -1037,7 +1044,14 @@ public override void GetStatisticsReturnsNewInstances() [Fact] public override async Task GetStatisticsHasCorrectValues() { - var limiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, false)); + 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); @@ -1089,5 +1103,38 @@ public override async Task GetStatisticsHasCorrectValues() 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.Acquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.WaitAndAcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.Acquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs index 7ea96cd6656a9b..b0380a0dcd6a4f 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/PartitionedRateLimiterTests.cs @@ -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 d453401370184a..9c56ca2dfcca15 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs @@ -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.Acquire(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.WaitAndAcquireAsync(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() { diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index 322cbf6fa6ea61..9b652eb6b62f25 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -1084,7 +1084,15 @@ public override async Task CanDisposeAfterCancelingQueuedRequest() [Fact] public override void GetStatisticsReturnsNewInstances() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, 2, false)); + 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); @@ -1100,7 +1108,15 @@ public override void GetStatisticsReturnsNewInstances() [Fact] public override async Task GetStatisticsHasCorrectValues() { - var limiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, 2, false)); + 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); @@ -1149,5 +1165,39 @@ public override async Task GetStatisticsHasCorrectValues() 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.Acquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.WaitAndAcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.Acquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs index ef8709b2c578db..8bc22e132116fb 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs @@ -1209,7 +1209,15 @@ public void ReplenishingRateLimiterPropertiesHaveCorrectValues() [Fact] public override void GetStatisticsReturnsNewInstances() { - var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.Zero, 2, false)); + 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); @@ -1225,7 +1233,15 @@ public override void GetStatisticsReturnsNewInstances() [Fact] public override async Task GetStatisticsHasCorrectValues() { - var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(100, QueueProcessingOrder.OldestFirst, 50, TimeSpan.Zero, 30, false)); + 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); @@ -1272,5 +1288,39 @@ public override async Task GetStatisticsHasCorrectValues() 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.Acquire(0); + Assert.True(lease.IsAcquired); + Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = await limiter.WaitAndAcquireAsync(0); + Assert.True(lease.IsAcquired); + Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); + + lease = limiter.Acquire(100); + Assert.True(lease.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + + var lease2 = limiter.Acquire(0); + Assert.False(lease2.IsAcquired); + Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); + Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); + Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); + } } } From 0c9d83cd4052ff131d46ce6ce6613f986109cb37 Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Tue, 9 Aug 2022 10:47:16 -0700 Subject: [PATCH 3/4] rebase --- .../tests/ChainedLimiterTests.cs | 6 +++--- .../tests/ConcurrencyLimiterTests.cs | 18 +++++++++--------- .../tests/FixedWindowRateLimiterTests.cs | 18 +++++++++--------- .../tests/RateLimiterPartitionTests.cs | 4 ++-- .../tests/SlidingWindowRateLimiterTests.cs | 18 +++++++++--------- .../tests/TokenBucketRateLimiterTests.cs | 18 +++++++++--------- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs index 050c6b7cd06db6..533364137ae46f 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs @@ -226,7 +226,7 @@ public void GetStatisticsHasCorrectValues() using var chainedLimiter = PartitionedRateLimiter.CreateChained(limiter1, limiter2, limiter3); - var lease = chainedLimiter.Acquire("", 10); + var lease = chainedLimiter.AttemptAcquire("", 10); var stats = chainedLimiter.GetStatistics(""); Assert.Equal(3, stats.CurrentAvailablePermits); @@ -234,7 +234,7 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(3, stats.TotalSuccessfulLeases); Assert.Equal(0, stats.TotalFailedLeases); - var lease2 = chainedLimiter.Acquire("", 10); + var lease2 = chainedLimiter.AttemptAcquire("", 10); Assert.False(lease2.IsAcquired); stats = chainedLimiter.GetStatistics(""); @@ -243,7 +243,7 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(5, stats.TotalSuccessfulLeases); Assert.Equal(1, stats.TotalFailedLeases); - var task = chainedLimiter.WaitAndAcquireAsync("", 10); + var task = chainedLimiter.AcquireAsync("", 10); Assert.False(task.IsCompleted); stats = chainedLimiter.GetStatistics(""); diff --git a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs index 6774b7e2b26f8b..ff260f0e2cd9d1 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs @@ -818,7 +818,7 @@ public override void GetStatisticsReturnsNewInstances() var stats = limiter.GetStatistics(); Assert.Equal(1, stats.CurrentAvailablePermits); - var lease = limiter.Acquire(1); + var lease = limiter.AttemptAcquire(1); var stats2 = limiter.GetStatistics(); Assert.NotSame(stats, stats2); @@ -843,7 +843,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(0, stats.TotalSuccessfulLeases); // success from acquire + available - var lease1 = limiter.Acquire(60); + var lease1 = limiter.AttemptAcquire(60); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); Assert.Equal(0, stats.CurrentQueuedCount); @@ -851,7 +851,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // queue - var lease2Task = limiter.WaitAndAcquireAsync(50); + var lease2Task = limiter.AcquireAsync(50); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); Assert.Equal(50, stats.CurrentQueuedCount); @@ -859,7 +859,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // failure from wait - var lease3 = await limiter.WaitAndAcquireAsync(1); + var lease3 = await limiter.AcquireAsync(1); Assert.False(lease3.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -868,7 +868,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // failure from acquire - var lease4 = limiter.Acquire(100); + var lease4 = limiter.AttemptAcquire(100); Assert.False(lease4.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -896,22 +896,22 @@ public override async Task GetStatisticsWithZeroPermitCount() QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 50 }); - var lease = limiter.Acquire(0); + var lease = limiter.AttemptAcquire(0); Assert.True(lease.IsAcquired); Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = await limiter.WaitAndAcquireAsync(0); + lease = await limiter.AcquireAsync(0); Assert.True(lease.IsAcquired); Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = limiter.Acquire(100); + lease = limiter.AttemptAcquire(100); Assert.True(lease.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); - var lease2 = limiter.Acquire(0); + var lease2 = limiter.AttemptAcquire(0); Assert.False(lease2.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); diff --git a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs index c032877e6809cf..d21ced08b79363 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -1033,7 +1033,7 @@ public override void GetStatisticsReturnsNewInstances() var stats = limiter.GetStatistics(); Assert.Equal(1, stats.CurrentAvailablePermits); - var lease = limiter.Acquire(1); + var lease = limiter.AttemptAcquire(1); var stats2 = limiter.GetStatistics(); Assert.NotSame(stats, stats2); @@ -1060,7 +1060,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(0, stats.TotalSuccessfulLeases); // success from acquire + available - var lease1 = limiter.Acquire(60); + var lease1 = limiter.AttemptAcquire(60); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); Assert.Equal(0, stats.CurrentQueuedCount); @@ -1068,7 +1068,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // queue - var lease2Task = limiter.WaitAndAcquireAsync(50); + var lease2Task = limiter.AcquireAsync(50); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); Assert.Equal(50, stats.CurrentQueuedCount); @@ -1076,7 +1076,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // failure from wait - var lease3 = await limiter.WaitAndAcquireAsync(1); + var lease3 = await limiter.AcquireAsync(1); Assert.False(lease3.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1085,7 +1085,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalSuccessfulLeases); // failure from acquire - var lease4 = limiter.Acquire(100); + var lease4 = limiter.AttemptAcquire(100); Assert.False(lease4.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1115,22 +1115,22 @@ public override async Task GetStatisticsWithZeroPermitCount() Window = TimeSpan.Zero, AutoReplenishment = false }); - var lease = limiter.Acquire(0); + var lease = limiter.AttemptAcquire(0); Assert.True(lease.IsAcquired); Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = await limiter.WaitAndAcquireAsync(0); + lease = await limiter.AcquireAsync(0); Assert.True(lease.IsAcquired); Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = limiter.Acquire(100); + lease = limiter.AttemptAcquire(100); Assert.True(lease.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); - var lease2 = limiter.Acquire(0); + var lease2 = limiter.AttemptAcquire(0); Assert.False(lease2.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); diff --git a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs index 9c56ca2dfcca15..94fe202307ee92 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/RateLimiterPartitionTests.cs @@ -86,7 +86,7 @@ public async Task NoLimiter_GetStatistics() var leaseCount = 0; for (var i = 0; i < 134; i++) { - var lease = limiter.Acquire(i); + var lease = limiter.AttemptAcquire(i); Assert.True(lease.IsAcquired); ++leaseCount; } @@ -99,7 +99,7 @@ public async Task NoLimiter_GetStatistics() for (var i = 0; i < 165; i++) { - var wait = limiter.WaitAndAcquireAsync(int.MaxValue); + var wait = limiter.AcquireAsync(int.MaxValue); Assert.True(wait.IsCompletedSuccessfully); var lease = await wait; Assert.True(lease.IsAcquired); diff --git a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index 9b652eb6b62f25..f8eb545f7ede78 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -1097,7 +1097,7 @@ public override void GetStatisticsReturnsNewInstances() var stats = limiter.GetStatistics(); Assert.Equal(1, stats.CurrentAvailablePermits); - var lease = limiter.Acquire(1); + var lease = limiter.AttemptAcquire(1); var stats2 = limiter.GetStatistics(); Assert.NotSame(stats, stats2); @@ -1124,14 +1124,14 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(0, stats.TotalFailedLeases); Assert.Equal(0, stats.TotalSuccessfulLeases); - var lease1 = limiter.Acquire(60); + 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.WaitAndAcquireAsync(50); + var lease2Task = limiter.AcquireAsync(50); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); Assert.Equal(50, stats.CurrentQueuedCount); @@ -1140,7 +1140,7 @@ public override async Task GetStatisticsHasCorrectValues() limiter.TryReplenish(); - var lease3 = await limiter.WaitAndAcquireAsync(1); + var lease3 = await limiter.AcquireAsync(1); Assert.False(lease3.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1148,7 +1148,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalFailedLeases); Assert.Equal(1, stats.TotalSuccessfulLeases); - var lease4 = limiter.Acquire(100); + var lease4 = limiter.AttemptAcquire(100); Assert.False(lease4.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1178,22 +1178,22 @@ public override async Task GetStatisticsWithZeroPermitCount() SegmentsPerWindow = 3, AutoReplenishment = false }); - var lease = limiter.Acquire(0); + var lease = limiter.AttemptAcquire(0); Assert.True(lease.IsAcquired); Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = await limiter.WaitAndAcquireAsync(0); + lease = await limiter.AcquireAsync(0); Assert.True(lease.IsAcquired); Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = limiter.Acquire(100); + lease = limiter.AttemptAcquire(100); Assert.True(lease.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); - var lease2 = limiter.Acquire(0); + var lease2 = limiter.AttemptAcquire(0); Assert.False(lease2.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); diff --git a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs index 8bc22e132116fb..278c114e64fa41 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs @@ -1222,7 +1222,7 @@ public override void GetStatisticsReturnsNewInstances() var stats = limiter.GetStatistics(); Assert.Equal(1, stats.CurrentAvailablePermits); - var lease = limiter.Acquire(1); + var lease = limiter.AttemptAcquire(1); var stats2 = limiter.GetStatistics(); Assert.NotSame(stats, stats2); @@ -1249,21 +1249,21 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(0, stats.TotalFailedLeases); Assert.Equal(0, stats.TotalSuccessfulLeases); - var lease1 = limiter.Acquire(60); + 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.WaitAndAcquireAsync(50); + 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.WaitAndAcquireAsync(1); + var lease3 = await limiter.AcquireAsync(1); Assert.False(lease3.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1271,7 +1271,7 @@ public override async Task GetStatisticsHasCorrectValues() Assert.Equal(1, stats.TotalFailedLeases); Assert.Equal(1, stats.TotalSuccessfulLeases); - var lease4 = limiter.Acquire(100); + var lease4 = limiter.AttemptAcquire(100); Assert.False(lease4.IsAcquired); stats = limiter.GetStatistics(); Assert.Equal(40, stats.CurrentAvailablePermits); @@ -1301,22 +1301,22 @@ public override async Task GetStatisticsWithZeroPermitCount() TokensPerPeriod = 30, AutoReplenishment = false }); - var lease = limiter.Acquire(0); + var lease = limiter.AttemptAcquire(0); Assert.True(lease.IsAcquired); Assert.Equal(1, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = await limiter.WaitAndAcquireAsync(0); + lease = await limiter.AcquireAsync(0); Assert.True(lease.IsAcquired); Assert.Equal(2, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(100, limiter.GetStatistics().CurrentAvailablePermits); - lease = limiter.Acquire(100); + lease = limiter.AttemptAcquire(100); Assert.True(lease.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(0, limiter.GetStatistics().CurrentAvailablePermits); - var lease2 = limiter.Acquire(0); + var lease2 = limiter.AttemptAcquire(0); Assert.False(lease2.IsAcquired); Assert.Equal(3, limiter.GetStatistics().TotalSuccessfulLeases); Assert.Equal(1, limiter.GetStatistics().TotalFailedLeases); From 955f12ccd577145303a08698d89ed3af6d4c5d63 Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Thu, 11 Aug 2022 18:33:12 -0700 Subject: [PATCH 4/4] fb --- .../ChainedPartitionedRateLimiter.cs | 6 +++--- .../RateLimiting/ConcurrencyLimiter.cs | 1 + .../RateLimiting/FixedWindowRateLimiter.cs | 1 + .../RateLimiting/PartitionedRateLimiter.T.cs | 8 +++----- .../RateLimiting/PartitionedRateLimiter.cs | 3 ++- .../RateLimiting/SlidingWindowRateLimiter.cs | 1 + .../RateLimiting/TokenBucketRateLimiter.cs | 1 + .../tests/BaseRateLimiterTests.cs | 3 +++ .../tests/ChainedLimiterTests.cs | 19 +++++++++++++++---- .../tests/ConcurrencyLimiterTests.cs | 13 +++++++++++++ .../tests/FixedWindowRateLimiterTests.cs | 15 +++++++++++++++ .../tests/SlidingWindowRateLimiterTests.cs | 16 ++++++++++++++++ .../tests/TokenBucketRateLimiterTests.cs | 16 ++++++++++++++++ 13 files changed, 90 insertions(+), 13 deletions(-) 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 f3047b941f9d59..91ebe1126d9c91 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 @@ -29,7 +29,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter long lowestAvailablePermits = long.MaxValue; long currentQueuedCount = 0; long totalFailedLeases = 0; - long totalSuccessfulLeases = 0; + long innerMostSuccessfulLeases = 0; foreach (PartitionedRateLimiter limiter in _limiters) { if (limiter.GetStatistics(resource) is { } statistics) @@ -40,7 +40,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter } currentQueuedCount += statistics.CurrentQueuedCount; totalFailedLeases += statistics.TotalFailedLeases; - totalSuccessfulLeases += statistics.TotalSuccessfulLeases; + innerMostSuccessfulLeases = statistics.TotalSuccessfulLeases; } } return new RateLimiterStatistics() @@ -48,7 +48,7 @@ public ChainedPartitionedRateLimiter(PartitionedRateLimiter[] limiter CurrentAvailablePermits = lowestAvailablePermits, CurrentQueuedCount = currentQueuedCount, TotalFailedLeases = totalFailedLeases, - TotalSuccessfulLeases = totalSuccessfulLeases, + TotalSuccessfulLeases = innerMostSuccessfulLeases, }; } 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 0b0e5e8cfeaacb..508340a2e138be 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 @@ -67,6 +67,7 @@ public ConcurrencyLimiter(ConcurrencyLimiterOptions options) /// public override RateLimiterStatistics? GetStatistics() { + ThrowIfDisposed(); return new RateLimiterStatistics() { CurrentAvailablePermits = _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 651db82896ba5c..fe4b0c29c3a627 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 @@ -86,6 +86,7 @@ public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options) /// public override RateLimiterStatistics? GetStatistics() { + ThrowIfDisposed(); return new RateLimiterStatistics() { CurrentAvailablePermits = _requestCount, 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 ee96362347b68a..b97ac2de2c1cb5 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 @@ -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). - /// should be implemented in a thread-safe way. + /// 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 . /// 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 2bfe22e3f94966..a2f592c5ea3b5d 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,8 @@ public static PartitionedRateLimiter Create /// Methods on the returned will iterate over the passed in in the order given. /// /// - /// will return the lowest value for + /// will return the lowest value for , + /// the inner-most limiter's , /// and the aggregate values for the rest of the properties from the . /// /// 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 6b3fd0955c3308..1ccf40775e2d87 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 @@ -94,6 +94,7 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options) /// public override RateLimiterStatistics? GetStatistics() { + ThrowIfDisposed(); return new RateLimiterStatistics() { CurrentAvailablePermits = _requestCount, 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 11b043dde3aebb..7baf91ea590804 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 @@ -88,6 +88,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options) /// public override RateLimiterStatistics? GetStatistics() { + ThrowIfDisposed(); return new RateLimiterStatistics() { CurrentAvailablePermits = _tokenCount, diff --git a/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs index 83c9a118cc4f68..e4adf232b9b5a4 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/BaseRateLimiterTests.cs @@ -124,5 +124,8 @@ public abstract class BaseRateLimiterTests [Fact] public abstract Task GetStatisticsWithZeroPermitCount(); + + [Fact] + public abstract void GetStatisticsThrowsAfterDispose(); } } diff --git a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs index 533364137ae46f..7310f2417531e9 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ChainedLimiterTests.cs @@ -194,7 +194,7 @@ public void GetStatisticsReturnsNewInstances() } [Fact] - public void GetStatisticsHasCorrectValues() + public async Task GetStatisticsHasCorrectValues() { using var limiter1 = PartitionedRateLimiter.Create(resource => { @@ -231,7 +231,7 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(3, stats.CurrentAvailablePermits); Assert.Equal(0, stats.CurrentQueuedCount); - Assert.Equal(3, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); Assert.Equal(0, stats.TotalFailedLeases); var lease2 = chainedLimiter.AttemptAcquire("", 10); @@ -240,7 +240,7 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(3, stats.CurrentAvailablePermits); Assert.Equal(0, stats.CurrentQueuedCount); - Assert.Equal(5, stats.TotalSuccessfulLeases); + Assert.Equal(1, stats.TotalSuccessfulLeases); Assert.Equal(1, stats.TotalFailedLeases); var task = chainedLimiter.AcquireAsync("", 10); @@ -249,7 +249,18 @@ public void GetStatisticsHasCorrectValues() Assert.Equal(2, stats.CurrentAvailablePermits); Assert.Equal(10, stats.CurrentQueuedCount); - Assert.Equal(7, stats.TotalSuccessfulLeases); + 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); } diff --git a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs index ff260f0e2cd9d1..54e2bbecc19752 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/ConcurrencyLimiterTests.cs @@ -917,5 +917,18 @@ public override async Task GetStatisticsWithZeroPermitCount() 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 d21ced08b79363..6830a1ce742816 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/FixedWindowRateLimiterTests.cs @@ -1136,5 +1136,20 @@ public override async Task GetStatisticsWithZeroPermitCount() 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/SlidingWindowRateLimiterTests.cs b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs index f8eb545f7ede78..7241a39e0f1f4d 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/SlidingWindowRateLimiterTests.cs @@ -1199,5 +1199,21 @@ public override async Task GetStatisticsWithZeroPermitCount() 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 278c114e64fa41..272c294a09b345 100644 --- a/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs +++ b/src/libraries/System.Threading.RateLimiting/tests/TokenBucketRateLimiterTests.cs @@ -1322,5 +1322,21 @@ public override async Task GetStatisticsWithZeroPermitCount() 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); + } } }