| area-System.Text.RegularExpressions | @ericstj | @buyaa-n @joperezr @steveharter | Consultants: @stephentoub |
| area-System.Threading | @mangod9 | @kouvel | |
| area-System.Threading.Channels | @ericstj | @buyaa-n @joperezr @steveharter | Consultants: @stephentoub |
+| area-System.Threading.RateLimiting | @rafikiassumani-msft | @BrennanConroy @halter73 | Consultants: @eerhardt |
| area-System.Threading.Tasks | @ericstj | @buyaa-n @joperezr @steveharter | Consultants: @stephentoub |
| area-System.Transactions | @HongGit | @HongGit | |
| area-System.Xml | @jeffhandley | @eiriktsarpalis @krwq @layomia | |
return _array[_head];
}
+ public T PeekTail()
+ {
+ Debug.Assert(!IsEmpty); // caller's responsibility to make sure there are elements remaining
+ var index = _tail - 1;
+ if (index == -1)
+ {
+ index = _array.Length - 1;
+ }
+ return _array[index];
+ }
+
public T DequeueTail()
{
Debug.Assert(!IsEmpty); // caller's responsibility to make sure there are elements remaining
</PropertyGroup>
<ItemGroup>
<Compile Include="System\VoidResult.cs" />
- <Compile Include="System\Collections\Generic\Deque.cs" />
<Compile Include="System\Threading\Channels\AsyncOperation.cs" />
<Compile Include="System\Threading\Channels\AsyncOperation.netcoreapp.cs"
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
Link="Common\Internal\Padding.cs" />
<Compile Include="$(CommonPath)System\Collections\Concurrent\SingleProducerConsumerQueue.cs"
Link="Common\System\Collections\Concurrent\SingleProducerConsumerQueue.cs" />
+ <Compile Include="$(CommonPath)System\Collections\Generic\Deque.cs"
+ Link="Common\System\Collections\Generic\Deque.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Reference Include="System.Collections" />
--- /dev/null
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{CAEE0409-CCC3-4EA6-AB54-177FD305D42D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj", "{F59F4FD7-EA00-47EA-A09A-6F76CB079F9B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{0D1C7DCB-970D-4099-AC9F-A01E75923EC6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{AF838F1D-5C1C-472B-B31C-9A3B7507BB4B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Interop.DllImportGenerator", "..\System.Runtime.InteropServices\gen\DllImportGenerator\DllImportGenerator.csproj", "{1E52F495-578C-4FDB-86DD-87EAAE0A0BE7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Interop.SourceGeneration", "..\System.Runtime.InteropServices\gen\Microsoft.Interop.SourceGeneration\Microsoft.Interop.SourceGeneration.csproj", "{25495BDC-0614-4FAC-B6EA-DF3F0E35A871}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Threading.RateLimiting", "ref\System.Threading.RateLimiting.csproj", "{FD274A80-0D68-48A0-9AC7-279C9E69BC63}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Threading.RateLimiting", "src\System.Threading.RateLimiting.csproj", "{CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Threading.RateLimiting.Tests", "tests\System.Threading.RateLimiting.Tests.csproj", "{AE81EE9F-1240-4AF1-BF21-7F451B7859E5}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6614EF7F-23FC-4809-AFF5-1ADBF1B6422C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{111B1B5B-A004-4C05-9A8C-E0931DADA5FB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CAEE0409-CCC3-4EA6-AB54-177FD305D42D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CAEE0409-CCC3-4EA6-AB54-177FD305D42D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CAEE0409-CCC3-4EA6-AB54-177FD305D42D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CAEE0409-CCC3-4EA6-AB54-177FD305D42D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F59F4FD7-EA00-47EA-A09A-6F76CB079F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F59F4FD7-EA00-47EA-A09A-6F76CB079F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F59F4FD7-EA00-47EA-A09A-6F76CB079F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F59F4FD7-EA00-47EA-A09A-6F76CB079F9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0D1C7DCB-970D-4099-AC9F-A01E75923EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0D1C7DCB-970D-4099-AC9F-A01E75923EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0D1C7DCB-970D-4099-AC9F-A01E75923EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0D1C7DCB-970D-4099-AC9F-A01E75923EC6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF838F1D-5C1C-472B-B31C-9A3B7507BB4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF838F1D-5C1C-472B-B31C-9A3B7507BB4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF838F1D-5C1C-472B-B31C-9A3B7507BB4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF838F1D-5C1C-472B-B31C-9A3B7507BB4B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E52F495-578C-4FDB-86DD-87EAAE0A0BE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E52F495-578C-4FDB-86DD-87EAAE0A0BE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E52F495-578C-4FDB-86DD-87EAAE0A0BE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E52F495-578C-4FDB-86DD-87EAAE0A0BE7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25495BDC-0614-4FAC-B6EA-DF3F0E35A871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25495BDC-0614-4FAC-B6EA-DF3F0E35A871}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25495BDC-0614-4FAC-B6EA-DF3F0E35A871}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25495BDC-0614-4FAC-B6EA-DF3F0E35A871}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD274A80-0D68-48A0-9AC7-279C9E69BC63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD274A80-0D68-48A0-9AC7-279C9E69BC63}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD274A80-0D68-48A0-9AC7-279C9E69BC63}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD274A80-0D68-48A0-9AC7-279C9E69BC63}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE81EE9F-1240-4AF1-BF21-7F451B7859E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE81EE9F-1240-4AF1-BF21-7F451B7859E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE81EE9F-1240-4AF1-BF21-7F451B7859E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE81EE9F-1240-4AF1-BF21-7F451B7859E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {CAEE0409-CCC3-4EA6-AB54-177FD305D42D} = {6614EF7F-23FC-4809-AFF5-1ADBF1B6422C}
+ {AE81EE9F-1240-4AF1-BF21-7F451B7859E5} = {6614EF7F-23FC-4809-AFF5-1ADBF1B6422C}
+ {39DA5B84-ECA2-42A2-BEBD-C056BDB8AD53} = {111B1B5B-A004-4C05-9A8C-E0931DADA5FB}
+ {0D1C7DCB-970D-4099-AC9F-A01E75923EC6} = {111B1B5B-A004-4C05-9A8C-E0931DADA5FB}
+ {FD274A80-0D68-48A0-9AC7-279C9E69BC63} = {111B1B5B-A004-4C05-9A8C-E0931DADA5FB}
+ {F59F4FD7-EA00-47EA-A09A-6F76CB079F9B} = {85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}
+ {AF838F1D-5C1C-472B-B31C-9A3B7507BB4B} = {85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}
+ {1E52F495-578C-4FDB-86DD-87EAAE0A0BE7} = {85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}
+ {25495BDC-0614-4FAC-B6EA-DF3F0E35A871} = {85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}
+ {CD96AFE9-0F7F-42FA-BBDA-F57EDCBB4609} = {85204CF5-0C88-4BBB-9E70-D8CCED82ED3D}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {25036AEF-71B3-4C8A-891F-0350414F9A23}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// ------------------------------------------------------------------------------
+// Changes to this file must follow the https://aka.ms/api-review process.
+// ------------------------------------------------------------------------------
+
+namespace System.Threading.RateLimiting
+{
+ public sealed partial class ConcurrencyLimiter : System.Threading.RateLimiting.RateLimiter
+ {
+ public ConcurrencyLimiter(System.Threading.RateLimiting.ConcurrencyLimiterOptions options) { }
+ protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount) { throw null; }
+ protected override void Dispose(bool disposing) { }
+ protected override System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; }
+ public override int GetAvailablePermits() { throw null; }
+ protected override System.Threading.Tasks.ValueTask<System.Threading.RateLimiting.RateLimitLease> WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ }
+ public sealed partial class ConcurrencyLimiterOptions
+ {
+ public ConcurrencyLimiterOptions(int permitLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit) { }
+ public int PermitLimit { get { throw null; } }
+ public int QueueLimit { get { throw null; } }
+ public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } }
+ }
+ public static partial class MetadataName
+ {
+ public static System.Threading.RateLimiting.MetadataName<string> ReasonPhrase { get { throw null; } }
+ public static System.Threading.RateLimiting.MetadataName<System.TimeSpan> RetryAfter { get { throw null; } }
+ public static System.Threading.RateLimiting.MetadataName<T> Create<T>(string name) { throw null; }
+ }
+ public sealed partial class MetadataName<T> : System.IEquatable<System.Threading.RateLimiting.MetadataName<T>>
+ {
+ public MetadataName(string name) { }
+ public string Name { get { throw null; } }
+ public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; }
+ public bool Equals(System.Threading.RateLimiting.MetadataName<T>? other) { throw null; }
+ public override int GetHashCode() { throw null; }
+ public static bool operator ==(System.Threading.RateLimiting.MetadataName<T> left, System.Threading.RateLimiting.MetadataName<T> right) { throw null; }
+ public static bool operator !=(System.Threading.RateLimiting.MetadataName<T> left, System.Threading.RateLimiting.MetadataName<T> right) { throw null; }
+ public override string ToString() { throw null; }
+ }
+ public enum QueueProcessingOrder
+ {
+ OldestFirst = 0,
+ NewestFirst = 1,
+ }
+ public abstract partial class RateLimiter : System.IAsyncDisposable, System.IDisposable
+ {
+ protected RateLimiter() { }
+ public System.Threading.RateLimiting.RateLimitLease Acquire(int permitCount = 1) { throw null; }
+ protected abstract System.Threading.RateLimiting.RateLimitLease AcquireCore(int permitCount);
+ 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 System.Threading.Tasks.ValueTask<System.Threading.RateLimiting.RateLimitLease> WaitAsync(int permitCount = 1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ protected abstract System.Threading.Tasks.ValueTask<System.Threading.RateLimiting.RateLimitLease> WaitAsyncCore(int permitCount, System.Threading.CancellationToken cancellationToken);
+ }
+ public abstract partial class RateLimitLease : System.IDisposable
+ {
+ protected RateLimitLease() { }
+ public abstract bool IsAcquired { get; }
+ public abstract System.Collections.Generic.IEnumerable<string> MetadataNames { get; }
+ public void Dispose() { }
+ protected virtual void Dispose(bool disposing) { }
+ public virtual System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, object?>> GetAllMetadata() { throw null; }
+ public abstract bool TryGetMetadata(string metadataName, out object? metadata);
+ public bool TryGetMetadata<T>(System.Threading.RateLimiting.MetadataName<T> metadataName, [System.Diagnostics.CodeAnalysis.MaybeNullAttribute] out T metadata) { throw null; }
+ }
+ public sealed partial class TokenBucketRateLimiter : System.Threading.RateLimiting.RateLimiter
+ {
+ public TokenBucketRateLimiter(System.Threading.RateLimiting.TokenBucketRateLimiterOptions options) { }
+ protected override System.Threading.RateLimiting.RateLimitLease AcquireCore(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 bool TryReplenish() { throw null; }
+ protected override System.Threading.Tasks.ValueTask<System.Threading.RateLimiting.RateLimitLease> WaitAsyncCore(int tokenCount, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ }
+ public sealed partial class TokenBucketRateLimiterOptions
+ {
+ public TokenBucketRateLimiterOptions(int tokenLimit, System.Threading.RateLimiting.QueueProcessingOrder queueProcessingOrder, int queueLimit, System.TimeSpan replenishmentPeriod, int tokensPerPeriod, bool autoReplenishment = true) { }
+ public bool AutoReplenishment { get { throw null; } }
+ public int QueueLimit { get { throw null; } }
+ public System.Threading.RateLimiting.QueueProcessingOrder QueueProcessingOrder { get { throw null; } }
+ public System.TimeSpan ReplenishmentPeriod { get { throw null; } }
+ public int TokenLimit { get { throw null; } }
+ public int TokensPerPeriod { get { throw null; } }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="System.Threading.RateLimiting.cs" />
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
+ <ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\ref\System.Runtime.csproj" />
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and '$(TargetFramework)' != '$(NetCoreAppCurrent)'">
+ <Reference Include="System.Runtime" />
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
+ <PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
+ </ItemGroup>
+ <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netstandard2.1'))">
+ <ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="TokenLimitExceeded" xml:space="preserve">
+ <value>{0} token(s) exceeds the token limit of {1}.</value>
+ </data>
+ <data name="PermitLimitExceeded" xml:space="preserve">
+ <value>{0} permit(s) exceeds the permit limit of {1}.</value>
+ </data>
+ <data name="ReplenishmentLimitTooHigh" xml:space="preserve">
+ <value>Over 49 days is not supported.</value>
+ </data>
+</root>
\ No newline at end of file
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
+ <Nullable>enable</Nullable>
+ <PackageDescription>APIs to help manage rate limiting.
+
+Commonly Used Types:
+System.Threading.RateLimiting.RateLimiter
+System.Threading.RateLimiting.ConcurrencyLimiter
+System.Threading.RateLimiting.TokenBucketRateLimiter
+System.Threading.RateLimiting.RateLimitLease</PackageDescription>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="System\Threading\RateLimiting\ConcurrencyLimiter.cs" />
+ <Compile Include="System\Threading\RateLimiting\ConcurrencyLimiterOptions.cs" />
+ <Compile Include="System\Threading\RateLimiting\MetadataName.cs" />
+ <Compile Include="System\Threading\RateLimiting\MetadataName.T.cs" />
+ <Compile Include="System\Threading\RateLimiting\QueueProcessingOrder.cs" />
+ <Compile Include="System\Threading\RateLimiting\RateLimiter.cs" />
+ <Compile Include="System\Threading\RateLimiting\RateLimitLease.cs" />
+ <Compile Include="System\Threading\RateLimiting\TokenBucketRateLimiter.cs" />
+ <Compile Include="System\Threading\RateLimiting\TokenBucketRateLimiterOptions.cs" />
+ <Compile Include="$(CommonPath)System\Collections\Generic\Deque.cs"
+ Link="Common\System\Collections\Generic\Deque.cs" />
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
+ <Reference Include="System.Runtime" />
+ <Reference Include="System.Threading" />
+ </ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
+ <PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
+ </ItemGroup>
+ <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netstandard2.1'))">
+ <ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj" />
+ </ItemGroup>
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace System.Threading.RateLimiting
+{
+ /// <summary>
+ /// <see cref="RateLimiter"/> implementation that helps manage concurrent access to a resource.
+ /// </summary>
+ public sealed class ConcurrencyLimiter : RateLimiter
+ {
+ private int _permitCount;
+ private int _queueCount;
+ private bool _disposed;
+
+ private readonly ConcurrencyLimiterOptions _options;
+ private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>();
+
+ private static readonly ConcurrencyLease SuccessfulLease = new ConcurrencyLease(true, null, 0);
+ private static readonly ConcurrencyLease FailedLease = new ConcurrencyLease(false, null, 0);
+ private static readonly ConcurrencyLease QueueLimitLease = new ConcurrencyLease(false, null, 0, "Queue limit reached");
+
+ // Use the queue as the lock field so we don't need to allocate another object for a lock and have another field in the object
+ private object Lock => _queue;
+
+ /// <summary>
+ /// Initializes the <see cref="ConcurrencyLimiter"/>.
+ /// </summary>
+ /// <param name="options">Options to specify the behavior of the <see cref="ConcurrencyLimiter"/>.</param>
+ public ConcurrencyLimiter(ConcurrencyLimiterOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _permitCount = _options.PermitLimit;
+ }
+
+ /// <inheritdoc/>
+ public override int GetAvailablePermits() => _permitCount;
+
+ /// <inheritdoc/>
+ protected override RateLimitLease AcquireCore(int permitCount)
+ {
+ // These amounts of resources can never be acquired
+ if (permitCount > _options.PermitLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(permitCount), permitCount, SR.Format(SR.PermitLimitExceeded, permitCount, _options.PermitLimit));
+ }
+
+ ThrowIfDisposed();
+
+ // Return SuccessfulLease or FailedLease to indicate limiter state
+ if (permitCount == 0)
+ {
+ return _permitCount > 0 ? SuccessfulLease : FailedLease;
+ }
+
+ // Perf: Check SemaphoreSlim implementation instead of locking
+ if (_permitCount >= permitCount)
+ {
+ lock (Lock)
+ {
+ if (TryLeaseUnsynchronized(permitCount, out RateLimitLease? lease))
+ {
+ return lease;
+ }
+ }
+ }
+
+ return FailedLease;
+ }
+
+ /// <inheritdoc/>
+ protected override ValueTask<RateLimitLease> WaitAsyncCore(int permitCount, CancellationToken cancellationToken = default)
+ {
+ // These amounts of resources can never be acquired
+ if (permitCount > _options.PermitLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(permitCount), permitCount, SR.Format(SR.PermitLimitExceeded, permitCount, _options.PermitLimit));
+ }
+
+ // Return SuccessfulLease if requestedCount is 0 and resources are available
+ if (permitCount == 0 && _permitCount > 0 && !_disposed)
+ {
+ return new ValueTask<RateLimitLease>(SuccessfulLease);
+ }
+
+ // Perf: Check SemaphoreSlim implementation instead of locking
+ lock (Lock)
+ {
+ if (TryLeaseUnsynchronized(permitCount, out RateLimitLease? lease))
+ {
+ return new ValueTask<RateLimitLease>(lease);
+ }
+
+ // Don't queue if queue limit reached
+ if (_queueCount + permitCount > _options.QueueLimit)
+ {
+ return new ValueTask<RateLimitLease>(QueueLimitLease);
+ }
+
+ TaskCompletionSource<RateLimitLease> tcs = new TaskCompletionSource<RateLimitLease>(TaskCreationOptions.RunContinuationsAsynchronously);
+ CancellationTokenRegistration ctr = default;
+ if (cancellationToken.CanBeCanceled)
+ {
+ ctr = cancellationToken.Register(static obj =>
+ {
+ ((TaskCompletionSource<RateLimitLease>)obj!).TrySetException(new OperationCanceledException());
+ }, tcs);
+ }
+
+ RequestRegistration request = new RequestRegistration(permitCount, tcs, ctr);
+ _queue.EnqueueTail(request);
+ _queueCount += permitCount;
+ Debug.Assert(_queueCount <= _options.QueueLimit);
+
+ return new ValueTask<RateLimitLease>(request.Tcs.Task);
+ }
+ }
+
+ private bool TryLeaseUnsynchronized(int permitCount, [NotNullWhen(true)] out RateLimitLease? lease)
+ {
+ ThrowIfDisposed();
+
+ // if permitCount is 0 we want to queue it if there are no available permits
+ if (_permitCount >= permitCount && _permitCount != 0)
+ {
+ if (permitCount == 0)
+ {
+ // Edge case where the check before the lock showed 0 available permits but when we got the lock some permits were now available
+ lease = SuccessfulLease;
+ return true;
+ }
+
+ // a. if there are no items queued we can lease
+ // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest
+ if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst))
+ {
+ _permitCount -= permitCount;
+ Debug.Assert(_permitCount >= 0);
+ lease = new ConcurrencyLease(true, this, permitCount);
+ return true;
+ }
+ }
+
+ lease = null;
+ return false;
+ }
+
+ private void Release(int releaseCount)
+ {
+ lock (Lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _permitCount += releaseCount;
+ Debug.Assert(_permitCount <= _options.PermitLimit);
+
+ while (_queue.Count > 0)
+ {
+ RequestRegistration nextPendingRequest =
+ _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? _queue.PeekHead()
+ : _queue.PeekTail();
+
+ if (_permitCount >= nextPendingRequest.Count)
+ {
+ nextPendingRequest =
+ _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? _queue.DequeueHead()
+ : _queue.DequeueTail();
+
+ _permitCount -= nextPendingRequest.Count;
+ _queueCount -= nextPendingRequest.Count;
+ Debug.Assert(_queueCount >= 0);
+ Debug.Assert(_permitCount >= 0);
+
+ ConcurrencyLease lease = nextPendingRequest.Count == 0 ? SuccessfulLease : new ConcurrencyLease(true, this, nextPendingRequest.Count);
+ // Check if request was canceled
+ if (!nextPendingRequest.Tcs.TrySetResult(lease))
+ {
+ // Queued item was canceled so add count back
+ _permitCount += nextPendingRequest.Count;
+ }
+ nextPendingRequest.CancellationTokenRegistration.Dispose();
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ lock (Lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ while (_queue.Count > 0)
+ {
+ RequestRegistration next = _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? _queue.DequeueHead()
+ : _queue.DequeueTail();
+ next.CancellationTokenRegistration.Dispose();
+ next.Tcs.SetResult(FailedLease);
+ }
+ }
+ }
+
+ protected override ValueTask DisposeAsyncCore()
+ {
+ Dispose(true);
+
+ return default;
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ConcurrencyLimiter));
+ }
+ }
+
+ private sealed class ConcurrencyLease : RateLimitLease
+ {
+ private static readonly string[] s_allMetadataNames = new[] { MetadataName.ReasonPhrase.Name };
+
+ private bool _disposed;
+ private readonly ConcurrencyLimiter? _limiter;
+ private readonly int _count;
+ private readonly string? _reason;
+
+ public ConcurrencyLease(bool isAcquired, ConcurrencyLimiter? limiter, int count, string? reason = null)
+ {
+ IsAcquired = isAcquired;
+ _limiter = limiter;
+ _count = count;
+ _reason = reason;
+
+ // No need to set the limiter if count is 0, Dispose will noop
+ Debug.Assert(count == 0 ? limiter is null : true);
+ }
+
+ public override bool IsAcquired { get; }
+
+ public override IEnumerable<string> MetadataNames => s_allMetadataNames;
+
+ public override bool TryGetMetadata(string metadataName, out object? metadata)
+ {
+ if (_reason is not null && metadataName == MetadataName.ReasonPhrase.Name)
+ {
+ metadata = _reason;
+ return true;
+ }
+ metadata = default;
+ return false;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ _limiter?.Release(_count);
+ }
+ }
+
+ private readonly struct RequestRegistration
+ {
+ public RequestRegistration(int requestedCount, TaskCompletionSource<RateLimitLease> tcs,
+ CancellationTokenRegistration cancellationTokenRegistration)
+ {
+ Count = requestedCount;
+ // Perf: Use AsyncOperation<TResult> instead
+ Tcs = tcs;
+ CancellationTokenRegistration = cancellationTokenRegistration;
+ }
+
+ public int Count { get; }
+
+ public TaskCompletionSource<RateLimitLease> Tcs { get; }
+
+ public CancellationTokenRegistration CancellationTokenRegistration { get; }
+ }
+ }
+}
--- /dev/null
+// 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
+{
+ /// <summary>
+ /// Options to specify the behavior of a <see cref="ConcurrencyLimiter"/>.
+ /// </summary>
+ public sealed class ConcurrencyLimiterOptions
+ {
+ /// <summary>
+ /// Initializes the <see cref="ConcurrencyLimiterOptions"/>.
+ /// </summary>
+ /// <param name="permitLimit">Maximum number of permits that can be leased concurrently.</param>
+ /// <param name="queueProcessingOrder">Determines the behaviour of <see cref="RateLimiter.WaitAsync"/> when not enough resources can be leased.</param>
+ /// <param name="queueLimit">Maximum number of permits that can be queued concurrently.</param>
+ /// <exception cref="ArgumentOutOfRangeException">When <paramref name="permitLimit"/> or <paramref name="queueLimit"/> are less than 0.</exception>
+ public ConcurrencyLimiterOptions(int permitLimit, QueueProcessingOrder queueProcessingOrder, int queueLimit)
+ {
+ if (permitLimit < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(permitLimit));
+ }
+ if (queueLimit < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(queueLimit));
+ }
+ PermitLimit = permitLimit;
+ QueueProcessingOrder = queueProcessingOrder;
+ QueueLimit = queueLimit;
+ }
+
+ /// <summary>
+ /// Maximum number of permits that can be leased concurrently.
+ /// </summary>
+ public int PermitLimit { get; }
+
+ /// <summary>
+ /// Determines the behaviour of <see cref="RateLimiter.WaitAsync"/> when not enough resources can be leased.
+ /// </summary>
+ /// <value>
+ /// <see cref="QueueProcessingOrder.OldestFirst"/> by default.
+ /// </value>
+ public QueueProcessingOrder QueueProcessingOrder { get; } = QueueProcessingOrder.OldestFirst;
+
+ /// <summary>
+ /// Maximum number of permits that can be queued concurrently.
+ /// </summary>
+ public int QueueLimit { get; }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Threading.RateLimiting
+{
+ /// <summary>
+ /// A strongly-typed name of metadata that can be stored in a <see cref="RateLimitLease"/>.
+ /// </summary>
+ /// <typeparam name="T">The type the metadata will be.</typeparam>
+ public sealed class MetadataName<T> : IEquatable<MetadataName<T>>
+ {
+ private readonly string _name;
+
+ /// <summary>
+ /// Constructs a <see cref="MetadataName{T}"/> object with the given name.
+ /// </summary>
+ /// <param name="name">The name of the <see cref="MetadataName"/> object.</param>
+ public MetadataName(string name)
+ {
+ _name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+
+ /// <summary>
+ /// Gets the name of the metadata.
+ /// </summary>
+ public string Name => _name;
+
+ /// <inheritdoc/>
+ public override string ToString()
+ {
+ return _name;
+ }
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ {
+ return _name.GetHashCode();
+ }
+
+ /// <inheritdoc/>
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is MetadataName<T> m && Equals(m);
+ }
+
+ /// <inheritdoc/>
+ public bool Equals(MetadataName<T>? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ return string.Equals(_name, other._name, StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Determines whether two <see cref="MetadataName{T}"/> are equal to each other.
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns></returns>
+ public static bool operator ==(MetadataName<T> left, MetadataName<T> right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// Determines whether two <see cref="MetadataName{T}"/> are not equal to each other.
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns></returns>
+ public static bool operator !=(MetadataName<T> left, MetadataName<T> right)
+ {
+ return !(left == right);
+ }
+ }
+}
--- /dev/null
+// 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
+{
+ /// <summary>
+ /// Contains some common metadata name-type pairs and helper method to create a metadata name.
+ /// </summary>
+ public static class MetadataName
+ {
+ /// <summary>
+ /// Metadata put on a failed lease acquisition to specify when to retry acquiring a lease.
+ /// For example, used in <see cref="TokenBucketRateLimiter"/> which periodically replenishes leases.
+ /// </summary>
+ public static MetadataName<TimeSpan> RetryAfter { get; } = Create<TimeSpan>("RETRY_AFTER");
+
+ /// <summary>
+ /// Metadata put on a failed lease acquisition to specify the reason the lease failed.
+ /// </summary>
+ public static MetadataName<string> ReasonPhrase { get; } = Create<string>("REASON_PHRASE");
+
+ /// <summary>
+ /// Create a strongly-typed metadata name.
+ /// </summary>
+ /// <typeparam name="T">Type that the metadata will contain.</typeparam>
+ /// <param name="name">Name of the metadata.</param>
+ /// <returns></returns>
+ public static MetadataName<T> Create<T>(string name) => new MetadataName<T>(name);
+ }
+}
--- /dev/null
+// 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
+{
+ /// <summary>
+ /// Controls the behavior of <see cref="RateLimiter.WaitAsync"/> when not enough resources can be leased.
+ /// </summary>
+ public enum QueueProcessingOrder
+ {
+ /// <summary>
+ /// Lease the oldest queued <see cref="RateLimiter.WaitAsync"/>.
+ /// </summary>
+ OldestFirst,
+ /// <summary>
+ /// Lease the newest queued <see cref="RateLimiter.WaitAsync"/>.
+ /// </summary>
+ NewestFirst
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Threading.RateLimiting
+{
+ /// <summary>
+ /// Abstraction for leases returned by <see cref="RateLimiter"/> implementations.
+ /// A lease represents the success or failure to acquire a resource and contains potential metadata that is relevant to the acquisition operation.
+ /// </summary>
+ public abstract class RateLimitLease : IDisposable
+ {
+ /// <summary>
+ /// Represents whether lease acquisition was successful.
+ /// </summary>
+ public abstract bool IsAcquired { get; }
+
+ /// <summary>
+ /// Attempt to extract metadata for the lease.
+ /// </summary>
+ /// <param name="metadataName">The name of the metadata. Some common ones can be found in <see cref="MetadataName"/>.</param>
+ /// <param name="metadata">The metadata object if it exists.</param>
+ /// <returns>True if the metadata exists, otherwise false.</returns>
+ public abstract bool TryGetMetadata(string metadataName, out object? metadata);
+
+ /// <summary>
+ /// Attempt to extract a strongly-typed metadata for the lease.
+ /// </summary>
+ /// <typeparam name="T">Type of the expected metadata.</typeparam>
+ /// <param name="metadataName">The name of the strongly-typed metadata. Some common ones can be found in <see cref="MetadataName"/>.</param>
+ /// <param name="metadata">The strongly-typed metadata object if it exists.</param>
+ /// <returns>True if the metadata exists, otherwise false.</returns>
+ public bool TryGetMetadata<T>(MetadataName<T> metadataName, [MaybeNull] out T metadata)
+ {
+ if (metadataName.Name == null)
+ {
+ metadata = default;
+ return false;
+ }
+
+ bool successful = TryGetMetadata(metadataName.Name, out object? rawMetadata);
+ if (successful)
+ {
+ metadata = rawMetadata is null ? default : (T)rawMetadata;
+ return true;
+ }
+
+ metadata = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Gets a list of the metadata names that are available on the lease.
+ /// </summary>
+ public abstract IEnumerable<string> MetadataNames { get; }
+
+ /// <summary>
+ /// Gets a list of all the metadata that is available on the lease.
+ /// </summary>
+ /// <returns>List of key-value pairs of metadata name and metadata object.</returns>
+ public virtual IEnumerable<KeyValuePair<string, object?>> GetAllMetadata()
+ {
+ foreach (string name in MetadataNames)
+ {
+ if (TryGetMetadata(name, out object? metadata))
+ {
+ yield return new KeyValuePair<string, object?>(name, metadata);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Dispose the lease. This may free up space on the limiter implementation the lease came from.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose method for implementations to write.
+ /// </summary>
+ /// <param name="disposing"></param>
+ protected virtual void Dispose(bool disposing) { }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+
+namespace System.Threading.RateLimiting
+{
+ /// <summary>
+ /// Represents a limiter type that users interact with to determine if an operation can proceed.
+ /// </summary>
+ public abstract class RateLimiter : IAsyncDisposable, IDisposable
+ {
+ /// <summary>
+ /// An estimated count of available permits.
+ /// </summary>
+ /// <returns></returns>
+ public abstract int GetAvailablePermits();
+
+ /// <summary>
+ /// Fast synchronous attempt to acquire permits.
+ /// </summary>
+ /// <remarks>
+ /// Set <paramref name="permitCount"/> to 0 to get whether permits are exhausted.
+ /// </remarks>
+ /// <param name="permitCount">Number of permits to try and acquire.</param>
+ /// <returns>A successful or failed lease.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public RateLimitLease Acquire(int permitCount = 1)
+ {
+ if (permitCount < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(permitCount));
+ }
+
+ return AcquireCore(permitCount);
+ }
+
+ /// <summary>
+ /// Method that <see cref="RateLimiter"/> implementations implement for <see cref="Acquire"/>.
+ /// </summary>
+ /// <param name="permitCount">Number of permits to try and acquire.</param>
+ /// <returns></returns>
+ protected abstract RateLimitLease AcquireCore(int permitCount);
+
+ /// <summary>
+ /// Wait until the requested permits are available or permits can no longer be acquired.
+ /// </summary>
+ /// <remarks>
+ /// Set <paramref name="permitCount"/> to 0 to wait until permits are replenished.
+ /// </remarks>
+ /// <param name="permitCount">Number of permits to try and acquire.</param>
+ /// <param name="cancellationToken">Optional token to allow canceling a queued request for permits.</param>
+ /// <returns>A task that completes when the requested permits are acquired or when the requested permits are denied.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public ValueTask<RateLimitLease> WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default)
+ {
+ if (permitCount < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(permitCount));
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return new ValueTask<RateLimitLease>(Task.FromCanceled<RateLimitLease>(cancellationToken));
+ }
+
+ return WaitAsyncCore(permitCount, cancellationToken);
+ }
+
+ /// <summary>
+ /// Method that <see cref="RateLimiter"/> implementations implement for <see cref="WaitAsync"/>.
+ /// </summary>
+ /// <param name="permitCount">Number of permits to try and acquire.</param>
+ /// <param name="cancellationToken">Optional token to allow canceling a queued request for permits.</param>
+ /// <returns>A task that completes when the requested permits are acquired or when the requested permits are denied.</returns>
+ protected abstract ValueTask<RateLimitLease> WaitAsyncCore(int permitCount, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Dispose method for implementations to write.
+ /// </summary>
+ /// <param name="disposing"></param>
+ protected virtual void Dispose(bool disposing) { }
+
+ /// <summary>
+ /// Disposes the RateLimiter. This completes any queued acquires with a failed lease.
+ /// </summary>
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// DisposeAsync method for implementations to write.
+ /// </summary>
+ protected virtual ValueTask DisposeAsyncCore()
+ {
+ return default;
+ }
+
+ /// <summary>
+ /// Disposes the RateLimiter asynchronously.
+ /// </summary>
+ /// <returns>ValueTask representin the completion of the disposal.</returns>
+ public async ValueTask DisposeAsync()
+ {
+ // Perform async cleanup.
+ await DisposeAsyncCore().ConfigureAwait(false);
+
+ // Dispose of unmanaged resources.
+ Dispose(false);
+
+ // Suppress finalization.
+ GC.SuppressFinalize(this);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace System.Threading.RateLimiting
+{
+ /// <summary>
+ /// <see cref="RateLimiter"/> implementation that replenishes tokens periodically instead of via a release mechanism.
+ /// </summary>
+ public sealed class TokenBucketRateLimiter : RateLimiter
+ {
+ private int _tokenCount;
+ private int _queueCount;
+ private uint _lastReplenishmentTick = (uint)Environment.TickCount;
+ private bool _disposed;
+
+ private readonly Timer? _renewTimer;
+ private readonly TokenBucketRateLimiterOptions _options;
+ private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>();
+
+ // Use the queue as the lock field so we don't need to allocate another object for a lock and have another field in the object
+ private object Lock => _queue;
+
+ private static readonly RateLimitLease SuccessfulLease = new TokenBucketLease(true, null);
+ private static readonly RateLimitLease FailedLease = new TokenBucketLease(false, null);
+
+ /// <summary>
+ /// Initializes the <see cref="TokenBucketRateLimiter"/>.
+ /// </summary>
+ /// <param name="options">Options to specify the behavior of the <see cref="TokenBucketRateLimiter"/>.</param>
+ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _tokenCount = options.TokenLimit;
+
+ if (_options.AutoReplenishment)
+ {
+ _renewTimer = new Timer(Replenish, this, _options.ReplenishmentPeriod, _options.ReplenishmentPeriod);
+ }
+ }
+
+ /// <inheritdoc/>
+ public override int GetAvailablePermits() => _tokenCount;
+
+ /// <inheritdoc/>
+ protected override RateLimitLease AcquireCore(int tokenCount)
+ {
+ // These amounts of resources can never be acquired
+ if (tokenCount > _options.TokenLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(tokenCount), tokenCount, SR.Format(SR.TokenLimitExceeded, tokenCount, _options.TokenLimit));
+ }
+
+ // Return SuccessfulLease or FailedLease depending to indicate limiter state
+ if (tokenCount == 0 && !_disposed)
+ {
+ if (_tokenCount > 0)
+ {
+ return SuccessfulLease;
+ }
+
+ return CreateFailedTokenLease(tokenCount);
+ }
+
+ lock (Lock)
+ {
+ if (TryLeaseUnsynchronized(tokenCount, out RateLimitLease? lease))
+ {
+ return lease;
+ }
+
+ return CreateFailedTokenLease(tokenCount);
+ }
+ }
+
+ /// <inheritdoc/>
+ protected override ValueTask<RateLimitLease> WaitAsyncCore(int tokenCount, CancellationToken cancellationToken = default)
+ {
+ // These amounts of resources can never be acquired
+ if (tokenCount > _options.TokenLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(tokenCount), tokenCount, SR.Format(SR.TokenLimitExceeded, tokenCount, _options.TokenLimit));
+ }
+
+ ThrowIfDisposed();
+
+ // Return SuccessfulAcquisition if requestedCount is 0 and resources are available
+ if (tokenCount == 0 && _tokenCount > 0)
+ {
+ return new ValueTask<RateLimitLease>(SuccessfulLease);
+ }
+
+ lock (Lock)
+ {
+ if (TryLeaseUnsynchronized(tokenCount, out RateLimitLease? lease))
+ {
+ return new ValueTask<RateLimitLease>(lease);
+ }
+
+ // Don't queue if queue limit reached
+ if (_queueCount + tokenCount > _options.QueueLimit)
+ {
+ return new ValueTask<RateLimitLease>(CreateFailedTokenLease(tokenCount));
+ }
+
+ TaskCompletionSource<RateLimitLease> tcs = new TaskCompletionSource<RateLimitLease>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ CancellationTokenRegistration ctr = default;
+ if (cancellationToken.CanBeCanceled)
+ {
+ ctr = cancellationToken.Register(static obj =>
+ {
+ ((TaskCompletionSource<RateLimitLease>)obj!).TrySetException(new OperationCanceledException());
+ }, tcs);
+ }
+
+ RequestRegistration registration = new RequestRegistration(tokenCount, tcs, ctr);
+ _queue.EnqueueTail(registration);
+ _queueCount += tokenCount;
+ Debug.Assert(_queueCount <= _options.QueueLimit);
+
+ // handle cancellation
+ return new ValueTask<RateLimitLease>(registration.Tcs.Task);
+ }
+ }
+
+ private RateLimitLease CreateFailedTokenLease(int tokenCount)
+ {
+ int replenishAmount = tokenCount - _tokenCount + _queueCount;
+ // can't have 0 replenish periods, that would mean it should be a successful lease
+ // if TokensPerPeriod is larger than the replenishAmount needed then it would be 0
+ Debug.Assert(_options.TokensPerPeriod > 0);
+ int replenishPeriods = Math.Max(replenishAmount / _options.TokensPerPeriod, 1);
+
+ return new TokenBucketLease(false, TimeSpan.FromTicks(_options.ReplenishmentPeriod.Ticks * replenishPeriods));
+ }
+
+ private bool TryLeaseUnsynchronized(int tokenCount, [NotNullWhen(true)] out RateLimitLease? lease)
+ {
+ ThrowIfDisposed();
+
+ // if permitCount is 0 we want to queue it if there are no available permits
+ if (_tokenCount >= tokenCount && _tokenCount != 0)
+ {
+ if (tokenCount == 0)
+ {
+ // Edge case where the check before the lock showed 0 available permits but when we got the lock some permits were now available
+ lease = SuccessfulLease;
+ return true;
+ }
+
+ // a. if there are no items queued we can lease
+ // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest
+ if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst))
+ {
+ _tokenCount -= tokenCount;
+ Debug.Assert(_tokenCount >= 0);
+ lease = SuccessfulLease;
+ return true;
+ }
+ }
+
+ lease = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to replenish the bucket.
+ /// </summary>
+ /// <returns>
+ /// False if <see cref="TokenBucketRateLimiterOptions.AutoReplenishment"/> is enabled, otherwise true.
+ /// Does not reflect if tokens were replenished.
+ /// </returns>
+ public bool TryReplenish()
+ {
+ if (_options.AutoReplenishment)
+ {
+ return false;
+ }
+ Replenish(this);
+ return true;
+ }
+
+ private static void Replenish(object? state)
+ {
+ TokenBucketRateLimiter limiter = (state as TokenBucketRateLimiter)!;
+ Debug.Assert(limiter is not null);
+
+ // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change
+ uint nowTicks = (uint)Environment.TickCount;
+ limiter!.ReplenishInternal(nowTicks);
+ }
+
+ // Used in tests that test behavior with specific time intervals
+ private void ReplenishInternal(uint nowTicks)
+ {
+ bool wrapped = false;
+ // (uint)TickCount will wrap every ~50 days, we can detect that by checking if the new ticks is less than the last replenishment
+ if (nowTicks < _lastReplenishmentTick)
+ {
+ wrapped = true;
+ }
+
+ // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes
+ lock (Lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ // Fix the wrapping by using a long and adding uint.MaxValue in the wrapped case
+ long nonWrappedTicks = wrapped ? (long)nowTicks + uint.MaxValue : nowTicks;
+ if (nonWrappedTicks - _lastReplenishmentTick < _options.ReplenishmentPeriod.TotalMilliseconds)
+ {
+ return;
+ }
+
+ _lastReplenishmentTick = nowTicks;
+
+ int availablePermits = _tokenCount;
+ TokenBucketRateLimiterOptions options = _options;
+ int maxPermits = options.TokenLimit;
+ int resourcesToAdd;
+
+ if (availablePermits < maxPermits)
+ {
+ resourcesToAdd = Math.Min(options.TokensPerPeriod, maxPermits - availablePermits);
+ }
+ else
+ {
+ // All tokens available, nothing to do
+ return;
+ }
+
+ // Process queued requests
+ Deque<RequestRegistration> queue = _queue;
+
+ _tokenCount += resourcesToAdd;
+ Debug.Assert(_tokenCount <= _options.TokenLimit);
+ while (queue.Count > 0)
+ {
+ RequestRegistration nextPendingRequest =
+ options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? queue.PeekHead()
+ : queue.PeekTail();
+
+ if (_tokenCount >= nextPendingRequest.Count)
+ {
+ // Request can be fulfilled
+ nextPendingRequest =
+ options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? queue.DequeueHead()
+ : queue.DequeueTail();
+
+ _queueCount -= nextPendingRequest.Count;
+ _tokenCount -= nextPendingRequest.Count;
+ Debug.Assert(_queueCount >= 0);
+ Debug.Assert(_tokenCount >= 0);
+
+ if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease))
+ {
+ // Queued item was canceled so add count back
+ _tokenCount += nextPendingRequest.Count;
+ }
+ nextPendingRequest.CancellationTokenRegistration.Dispose();
+ }
+ else
+ {
+ // Request cannot be fulfilled
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ lock (Lock)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ _renewTimer?.Dispose();
+ while (_queue.Count > 0)
+ {
+ RequestRegistration next = _options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
+ ? _queue.DequeueHead()
+ : _queue.DequeueTail();
+ next.CancellationTokenRegistration.Dispose();
+ next.Tcs.SetResult(FailedLease);
+ }
+ }
+ }
+
+ protected override ValueTask DisposeAsyncCore()
+ {
+ Dispose(true);
+
+ return default;
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(TokenBucketRateLimiter));
+ }
+ }
+
+ private sealed class TokenBucketLease : RateLimitLease
+ {
+ private static readonly string[] s_allMetadataNames = new[] { MetadataName.RetryAfter.Name };
+
+ private readonly TimeSpan? _retryAfter;
+
+ public TokenBucketLease(bool isAcquired, TimeSpan? retryAfter)
+ {
+ IsAcquired = isAcquired;
+ _retryAfter = retryAfter;
+ }
+
+ public override bool IsAcquired { get; }
+
+ public override IEnumerable<string> MetadataNames => s_allMetadataNames;
+
+ public override bool TryGetMetadata(string metadataName, out object? metadata)
+ {
+ if (metadataName == MetadataName.RetryAfter.Name && _retryAfter.HasValue)
+ {
+ metadata = _retryAfter.Value;
+ return true;
+ }
+
+ metadata = default;
+ return false;
+ }
+ }
+
+ private readonly struct RequestRegistration
+ {
+ public RequestRegistration(int tokenCount, TaskCompletionSource<RateLimitLease> tcs, CancellationTokenRegistration cancellationTokenRegistration)
+ {
+ Count = tokenCount;
+ // Use VoidAsyncOperationWithData<T> instead
+ Tcs = tcs;
+ CancellationTokenRegistration = cancellationTokenRegistration;
+ }
+
+ public int Count { get; }
+
+ public TaskCompletionSource<RateLimitLease> Tcs { get; }
+
+ public CancellationTokenRegistration CancellationTokenRegistration { get; }
+
+ }
+ }
+}
--- /dev/null
+// 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
+{
+ /// <summary>
+ /// Options to control the behavior of a <see cref="TokenBucketRateLimiter"/>.
+ /// </summary>
+ public sealed class TokenBucketRateLimiterOptions
+ {
+ /// <summary>
+ /// Initializes the <see cref="TokenBucketRateLimiterOptions"/>.
+ /// </summary>
+ /// <param name="tokenLimit">Maximum number of tokens that can be in the token bucket.</param>
+ /// <param name="queueProcessingOrder"></param>
+ /// <param name="queueLimit">Maximum number of unprocessed tokens waiting via <see cref="RateLimiter.WaitAsync(int, CancellationToken)"/>.</param>
+ /// <param name="replenishmentPeriod">
+ /// Specifies how often tokens can be replenished. Replenishing is triggered either by an internal timer if <paramref name="autoReplenishment"/> is true, or by calling <see cref="TokenBucketRateLimiter.TryReplenish"/>.
+ /// </param>
+ /// <param name="tokensPerPeriod">Specified how many tokens can be added to the token bucket on a successful replenish. Available token count will not exceed <paramref name="tokenLimit"/>.</param>
+ /// <param name="autoReplenishment">
+ /// Specifies whether token replenishment will be handled by the <see cref="TokenBucketRateLimiter"/> or by another party via <see cref="TokenBucketRateLimiter.TryReplenish"/>.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException">When <paramref name="tokenLimit"/>, <paramref name="queueLimit"/>, or <paramref name="tokensPerPeriod"/> are less than 0
+ /// or when <paramref name="replenishmentPeriod"/> is more than 49 days.</exception>
+ public TokenBucketRateLimiterOptions(
+ int tokenLimit,
+ QueueProcessingOrder queueProcessingOrder,
+ int queueLimit,
+ TimeSpan replenishmentPeriod,
+ int tokensPerPeriod,
+ bool autoReplenishment = true)
+ {
+ if (tokenLimit < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(tokenLimit));
+ }
+ if (queueLimit < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(queueLimit));
+ }
+ if (tokensPerPeriod <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(tokensPerPeriod));
+ }
+ if (replenishmentPeriod.TotalDays > 49)
+ {
+ // Environment.TickCount is an int and represents milliseconds since system started
+ // it has a range of -2B - +2B, we cast it to a uint to get a range of 0 - 4B which is 49.7 days before the value will repeat
+ throw new ArgumentOutOfRangeException(nameof(replenishmentPeriod), replenishmentPeriod, SR.ReplenishmentLimitTooHigh);
+ }
+
+ TokenLimit = tokenLimit;
+ QueueProcessingOrder = queueProcessingOrder;
+ QueueLimit = queueLimit;
+ ReplenishmentPeriod = replenishmentPeriod;
+ TokensPerPeriod = tokensPerPeriod;
+ AutoReplenishment = autoReplenishment;
+ }
+
+ /// <summary>
+ /// Specifies the minimum period between replenishments.
+ /// </summary>
+ public TimeSpan ReplenishmentPeriod { get; }
+
+ /// <summary>
+ /// Specifies the maximum number of tokens to restore each replenishment.
+ /// </summary>
+ public int TokensPerPeriod { get; }
+
+ /// <summary>
+ /// Specified whether the <see cref="TokenBucketRateLimiter"/> is automatically replenishing tokens or if someone else
+ /// will be calling <see cref="TokenBucketRateLimiter.TryReplenish"/> to replenish tokens.
+ /// </summary>
+ public bool AutoReplenishment { get; }
+
+ /// <summary>
+ /// Maximum number of tokens that can be in the bucket at any time.
+ /// </summary>
+ public int TokenLimit { get; }
+
+ /// <summary>
+ /// Determines the behaviour of <see cref="RateLimiter.WaitAsync"/> when not enough resources can be leased.
+ /// </summary>
+ /// <value>
+ /// <see cref="QueueProcessingOrder.OldestFirst"/> by default.
+ /// </value>
+ public QueueProcessingOrder QueueProcessingOrder { get; }
+
+ /// <summary>
+ /// Maximum cumulative token count of queued acquisition requests.
+ /// </summary>
+ public int QueueLimit { get; }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Threading.RateLimiting.Test
+{
+ public abstract class BaseRateLimiterTests
+ {
+ [Fact]
+ public abstract void CanAcquireResource();
+
+ [Fact]
+ public abstract void InvalidOptionsThrows();
+
+ [Fact]
+ public abstract Task CanAcquireResourceAsync();
+
+ [Fact]
+ public abstract Task CanAcquireResourceAsync_QueuesAndGrabsOldest();
+
+ [Fact]
+ public abstract Task CanAcquireResourceAsync_QueuesAndGrabsNewest();
+
+ [Fact]
+ public abstract Task FailsWhenQueuingMoreThanLimit();
+
+ [Fact]
+ public abstract Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable();
+
+ [Fact]
+ public abstract void ThrowsWhenAcquiringMoreThanLimit();
+
+ [Fact]
+ public abstract Task ThrowsWhenWaitingForMoreThanLimit();
+
+ [Fact]
+ public abstract void ThrowsWhenAcquiringLessThanZero();
+
+ [Fact]
+ public abstract Task ThrowsWhenWaitingForLessThanZero();
+
+ [Fact]
+ public abstract void AcquireZero_WithAvailability();
+
+ [Fact]
+ public abstract void AcquireZero_WithoutAvailability();
+
+ [Fact]
+ public abstract Task WaitAsyncZero_WithAvailability();
+
+ [Fact]
+ public abstract Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability();
+
+ [Fact]
+ public abstract Task CanDequeueMultipleResourcesAtOnce();
+
+ [Fact]
+ public abstract Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst();
+
+ [Fact]
+ public abstract Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst();
+
+ [Fact]
+ public abstract Task CanCancelWaitAsyncAfterQueuing();
+
+ [Fact]
+ public abstract Task CanCancelWaitAsyncBeforeQueuing();
+
+ [Fact]
+ public abstract Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst();
+
+ [Fact]
+ public abstract Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst();
+
+ [Fact]
+ public abstract void NoMetadataOnAcquiredLease();
+
+ [Fact]
+ public abstract void MetadataNamesContainsAllMetadata();
+
+ [Fact]
+ public abstract Task DisposeReleasesQueuedAcquires();
+
+ [Fact]
+ public abstract Task DisposeAsyncReleasesQueuedAcquires();
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Threading.RateLimiting.Test
+{
+ public class ConcurrencyLimiterTests : BaseRateLimiterTests
+ {
+ [Fact]
+ public override void InvalidOptionsThrows()
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => new ConcurrencyLimiterOptions(-1, QueueProcessingOrder.NewestFirst, 1));
+ Assert.Throws<ArgumentOutOfRangeException>(() => new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, -1));
+ }
+
+ [Fact]
+ public override void CanAcquireResource()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var lease = limiter.Acquire();
+
+ Assert.True(lease.IsAcquired);
+ Assert.False(limiter.Acquire().IsAcquired);
+
+ lease.Dispose();
+
+ Assert.True(limiter.Acquire().IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var lease = await limiter.WaitAsync();
+
+ Assert.True(lease.IsAcquired);
+ var wait = limiter.WaitAsync();
+ Assert.False(wait.IsCompleted);
+
+ lease.Dispose();
+
+ Assert.True((await wait).IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2));
+ var lease = await limiter.WaitAsync();
+
+ Assert.True(lease.IsAcquired);
+ var wait1 = limiter.WaitAsync();
+ var wait2 = limiter.WaitAsync();
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+
+ lease = await wait1;
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+
+ lease = await wait2;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3));
+ var lease = await limiter.WaitAsync(2);
+
+ Assert.True(lease.IsAcquired);
+ var wait1 = limiter.WaitAsync(2);
+ var wait2 = limiter.WaitAsync();
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+
+ // second queued item completes first with NewestFirst
+ lease = await wait2;
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait1.IsCompleted);
+
+ lease.Dispose();
+
+ lease = await wait1;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task FailsWhenQueuingMoreThanLimit()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ using var lease = limiter.Acquire(1);
+ var wait = limiter.WaitAsync(1);
+
+ var failedLease = await limiter.WaitAsync(1);
+ Assert.False(failedLease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var lease = limiter.Acquire(1);
+ var wait = limiter.WaitAsync(1);
+
+ var failedLease = await limiter.WaitAsync(1);
+ Assert.False(failedLease.IsAcquired);
+
+ lease.Dispose();
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+
+ wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ lease.Dispose();
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override void ThrowsWhenAcquiringMoreThanLimit()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var ex = Assert.Throws<ArgumentOutOfRangeException>(() => limiter.Acquire(2));
+ Assert.Equal("permitCount", ex.ParamName);
+ }
+
+ [Fact]
+ public override async Task ThrowsWhenWaitingForMoreThanLimit()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var ex = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.WaitAsync(2));
+ Assert.Equal("permitCount", ex.ParamName);
+ }
+
+ [Fact]
+ public override void ThrowsWhenAcquiringLessThanZero()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ Assert.Throws<ArgumentOutOfRangeException>(() => limiter.Acquire(-1));
+ }
+
+ [Fact]
+ public override async Task ThrowsWhenWaitingForLessThanZero()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.WaitAsync(-1));
+ }
+
+ [Fact]
+ public override void AcquireZero_WithAvailability()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+
+ using var lease = limiter.Acquire(0);
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override void AcquireZero_WithoutAvailability()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ using var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var lease2 = limiter.Acquire(0);
+ Assert.False(lease2.IsAcquired);
+ lease2.Dispose();
+ }
+
+ [Fact]
+ public override async Task WaitAsyncZero_WithAvailability()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+
+ using var lease = await limiter.WaitAsync(0);
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
+ var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(0);
+ Assert.False(wait.IsCompleted);
+
+ lease.Dispose();
+ using var lease2 = await wait;
+ Assert.True(lease2.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanDequeueMultipleResourcesAtOnce()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2));
+ using var lease = await limiter.WaitAsync(2);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+
+ var lease1 = await wait1;
+ var lease2 = await wait2;
+ Assert.True(lease1.IsAcquired);
+ Assert.True(lease2.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3));
+ using var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(2);
+ Assert.False(wait1.IsCompleted);
+ var wait2 = limiter.WaitAsync(1);
+ var lease2 = await wait2;
+ Assert.True(lease2.IsAcquired);
+
+ lease.Dispose();
+
+ Assert.False(wait1.IsCompleted);
+ lease2.Dispose();
+
+ var lease1 = await wait1;
+ Assert.True(lease1.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3));
+ using var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(2);
+ var wait2 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+
+ var lease1 = await wait1;
+ Assert.True(lease1.IsAcquired);
+ Assert.False(wait2.IsCompleted);
+
+ lease1.Dispose();
+ var lease2 = await wait2;
+ Assert.True(lease2.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3));
+ using var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(2);
+ Assert.False(wait1.IsCompleted);
+ var lease2 = limiter.Acquire(1);
+ Assert.True(lease2.IsAcquired);
+
+ lease.Dispose();
+
+ Assert.False(wait1.IsCompleted);
+ lease2.Dispose();
+
+ var lease1 = await wait1;
+ Assert.True(lease1.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3));
+ using var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(2);
+ Assert.False(wait1.IsCompleted);
+ var lease2 = limiter.Acquire(1);
+ Assert.False(lease2.IsAcquired);
+
+ lease.Dispose();
+
+ var lease1 = await wait1;
+ Assert.True(lease1.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanCancelWaitAsyncAfterQueuing()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1));
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var cts = new CancellationTokenSource();
+ var wait = limiter.WaitAsync(1, cts.Token);
+
+ cts.Cancel();
+ await Assert.ThrowsAsync<OperationCanceledException>(() => wait.AsTask());
+
+ lease.Dispose();
+
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public override async Task CanCancelWaitAsyncBeforeQueuing()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1));
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ await Assert.ThrowsAsync<TaskCanceledException>(() => limiter.WaitAsync(1, cts.Token).AsTask());
+
+ lease.Dispose();
+
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public override void NoMetadataOnAcquiredLease()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1));
+ using var lease = limiter.Acquire(1);
+ Assert.False(lease.TryGetMetadata(MetadataName.ReasonPhrase.Name, out _));
+ }
+
+ [Fact]
+ public override void MetadataNamesContainsAllMetadata()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1));
+ using var lease = limiter.Acquire(1);
+ Assert.Collection(lease.MetadataNames, metadataName => Assert.Equal(metadataName, MetadataName.ReasonPhrase.Name));
+ }
+
+ [Fact]
+ public override async Task DisposeReleasesQueuedAcquires()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3));
+ using var lease = limiter.Acquire(1);
+
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ var wait3 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+ Assert.False(wait3.IsCompleted);
+
+ limiter.Dispose();
+
+ var failedLease = await wait1;
+ Assert.False(failedLease.IsAcquired);
+ failedLease = await wait2;
+ Assert.False(failedLease.IsAcquired);
+ failedLease = await wait3;
+ Assert.False(failedLease.IsAcquired);
+
+ lease.Dispose();
+
+ // Throws after disposal
+ Assert.Throws<ObjectDisposedException>(() => limiter.Acquire(1));
+ await Assert.ThrowsAsync<ObjectDisposedException>(() => limiter.WaitAsync(1).AsTask());
+ }
+
+ [Fact]
+ public override async Task DisposeAsyncReleasesQueuedAcquires()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3));
+ using var lease = limiter.Acquire(1);
+
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ var wait3 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+ Assert.False(wait3.IsCompleted);
+
+ await limiter.DisposeAsync();
+
+ var failedLease = await wait1;
+ Assert.False(failedLease.IsAcquired);
+ failedLease = await wait2;
+ Assert.False(failedLease.IsAcquired);
+ failedLease = await wait3;
+ Assert.False(failedLease.IsAcquired);
+
+ lease.Dispose();
+
+ // Throws after disposal
+ Assert.Throws<ObjectDisposedException>(() => limiter.Acquire(1));
+ await Assert.ThrowsAsync<ObjectDisposedException>(() => limiter.WaitAsync(1).AsTask());
+ }
+
+ [Fact]
+ public async Task ReasonMetadataOnFailedWaitAsync()
+ {
+ var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1));
+ using var lease = limiter.Acquire(2);
+
+ var failedLease = await limiter.WaitAsync(2);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.ReasonPhrase.Name, out var metadata));
+ Assert.Equal("Queue limit reached", metadata);
+
+ Assert.True(failedLease.TryGetMetadata(MetadataName.ReasonPhrase, out var typedMetadata));
+ Assert.Equal("Queue limit reached", typedMetadata);
+ Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.ReasonPhrase.Name));
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkMinimum)</TargetFrameworks>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="BaseRateLimiterTests.cs" />
+ <Compile Include="ConcurrencyLimiterTests.cs" />
+ <Compile Include="TokenBucketRateLimiterTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\src\System.Threading.RateLimiting.csproj" />
+ </ItemGroup>
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Threading.RateLimiting.Test
+{
+ public class TokenBucketRateLimiterTests : BaseRateLimiterTests
+ {
+ [Fact]
+ public override void CanAcquireResource()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire();
+
+ Assert.True(lease.IsAcquired);
+ Assert.False(limiter.Acquire().IsAcquired);
+
+ lease.Dispose();
+ Assert.False(limiter.Acquire().IsAcquired);
+ Assert.True(limiter.TryReplenish());
+
+ Assert.True(limiter.Acquire().IsAcquired);
+ }
+
+ [Fact]
+ public override void InvalidOptionsThrows()
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => new TokenBucketRateLimiterOptions(-1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromMinutes(2), 1, autoReplenishment: false));
+ Assert.Throws<ArgumentOutOfRangeException>(() => new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, -1, TimeSpan.FromMinutes(2), 1, autoReplenishment: false));
+ Assert.Throws<ArgumentOutOfRangeException>(() => new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromMinutes(2), -1, autoReplenishment: false));
+ Assert.Throws<ArgumentOutOfRangeException>(() => new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1, TimeSpan.FromDays(49).Add(TimeSpan.FromMilliseconds(1)), 1, autoReplenishment: false));
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+
+ using var lease = await limiter.WaitAsync();
+
+ Assert.True(lease.IsAcquired);
+ var wait = limiter.WaitAsync();
+ Assert.False(wait.IsCompleted);
+
+ Assert.True(limiter.TryReplenish());
+
+ Assert.True((await wait).IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync_QueuesAndGrabsOldest()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 2,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = await limiter.WaitAsync();
+
+ Assert.True(lease.IsAcquired);
+ var wait1 = limiter.WaitAsync();
+ var wait2 = limiter.WaitAsync();
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+
+ lease = await wait1;
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+ Assert.Equal(0, limiter.GetAvailablePermits());
+ Assert.True(limiter.TryReplenish());
+
+ lease = await wait2;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourceAsync_QueuesAndGrabsNewest()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3,
+ TimeSpan.FromMinutes(0), 1, autoReplenishment: false));
+
+ var lease = await limiter.WaitAsync(2);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(2);
+ var wait2 = limiter.WaitAsync();
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+
+ // second queued item completes first with NewestFirst
+ lease = await wait2;
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait1.IsCompleted);
+
+ lease.Dispose();
+ Assert.Equal(0, limiter.GetAvailablePermits());
+ Assert.True(limiter.TryReplenish());
+ Assert.True(limiter.TryReplenish());
+
+ lease = await wait1;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task FailsWhenQueuingMoreThanLimit()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ using var lease = limiter.Acquire(1);
+ var wait = limiter.WaitAsync(1);
+
+ var failedLease = await limiter.WaitAsync(1);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var timeSpan));
+ Assert.Equal(TimeSpan.Zero, timeSpan);
+ }
+
+ [Fact]
+ public override async Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire(1);
+ var wait = limiter.WaitAsync(1);
+
+ var failedLease = await limiter.WaitAsync(1);
+ Assert.False(failedLease.IsAcquired);
+
+ limiter.TryReplenish();
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+
+ wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ limiter.TryReplenish();
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override void ThrowsWhenAcquiringMoreThanLimit()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ Assert.Throws<ArgumentOutOfRangeException>(() => limiter.Acquire(2));
+ }
+
+ [Fact]
+ public override async Task ThrowsWhenWaitingForMoreThanLimit()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.WaitAsync(2));
+ }
+
+ [Fact]
+ public override void ThrowsWhenAcquiringLessThanZero()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ Assert.Throws<ArgumentOutOfRangeException>(() => limiter.Acquire(-1));
+ }
+
+ [Fact]
+ public override async Task ThrowsWhenWaitingForLessThanZero()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await limiter.WaitAsync(-1));
+ }
+
+ [Fact]
+ public override void AcquireZero_WithAvailability()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+
+ using var lease = limiter.Acquire(0);
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override void AcquireZero_WithoutAvailability()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ using var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var lease2 = limiter.Acquire(0);
+ Assert.False(lease2.IsAcquired);
+ lease2.Dispose();
+ }
+
+ [Fact]
+ public override async Task WaitAsyncZero_WithAvailability()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+
+ using var lease = await limiter.WaitAsync(0);
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(0);
+ Assert.False(wait.IsCompleted);
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+ using var lease2 = await wait;
+ Assert.True(lease2.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanDequeueMultipleResourcesAtOnce()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2,
+ TimeSpan.Zero, 2, autoReplenishment: false));
+ using var lease = await limiter.WaitAsync(2);
+ Assert.True(lease.IsAcquired);
+
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+
+ var lease1 = await wait1;
+ var lease2 = await wait2;
+ Assert.True(lease1.IsAcquired);
+ Assert.True(lease2.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanCancelWaitAsyncAfterQueuing()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var cts = new CancellationTokenSource();
+ var wait = limiter.WaitAsync(1, cts.Token);
+
+ cts.Cancel();
+ await Assert.ThrowsAsync<OperationCanceledException>(() => wait.AsTask());
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public override async Task CanCancelWaitAsyncBeforeQueuing()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ await Assert.ThrowsAsync<TaskCanceledException>(() => limiter.WaitAsync(1, cts.Token).AsTask());
+
+ lease.Dispose();
+ Assert.True(limiter.TryReplenish());
+
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public override void NoMetadataOnAcquiredLease()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ using var lease = limiter.Acquire(1);
+ Assert.False(lease.TryGetMetadata(MetadataName.RetryAfter, out _));
+ }
+
+ [Fact]
+ public override void MetadataNamesContainsAllMetadata()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ using var lease = limiter.Acquire(1);
+ Assert.Collection(lease.MetadataNames, metadataName => Assert.Equal(metadataName, MetadataName.RetryAfter.Name));
+ }
+
+ [Fact]
+ public override async Task DisposeReleasesQueuedAcquires()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire(1);
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ var wait3 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+ Assert.False(wait3.IsCompleted);
+
+ limiter.Dispose();
+
+ lease = await wait1;
+ Assert.False(lease.IsAcquired);
+ lease = await wait2;
+ Assert.False(lease.IsAcquired);
+ lease = await wait3;
+ Assert.False(lease.IsAcquired);
+
+ // Throws after disposal
+ Assert.Throws<ObjectDisposedException>(() => limiter.Acquire(1));
+ await Assert.ThrowsAsync<ObjectDisposedException>(() => limiter.WaitAsync(1).AsTask());
+ }
+
+ [Fact]
+ public override async Task DisposeAsyncReleasesQueuedAcquires()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 3,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ var lease = limiter.Acquire(1);
+ var wait1 = limiter.WaitAsync(1);
+ var wait2 = limiter.WaitAsync(1);
+ var wait3 = limiter.WaitAsync(1);
+ Assert.False(wait1.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+ Assert.False(wait3.IsCompleted);
+
+ await limiter.DisposeAsync();
+
+ lease = await wait1;
+ Assert.False(lease.IsAcquired);
+ lease = await wait2;
+ Assert.False(lease.IsAcquired);
+ lease = await wait3;
+ Assert.False(lease.IsAcquired);
+
+ // Throws after disposal
+ Assert.Throws<ObjectDisposedException>(() => limiter.Acquire(1));
+ await Assert.ThrowsAsync<ObjectDisposedException>(() => limiter.WaitAsync(1).AsTask());
+ }
+
+ [Fact]
+ public async Task RetryMetadataOnFailedWaitAsync()
+ {
+ var options = new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(20), 1, autoReplenishment: false);
+ var limiter = new TokenBucketRateLimiter(options);
+
+ using var lease = limiter.Acquire(2);
+
+ var failedLease = await limiter.WaitAsync(2);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter.Name, out var metadata));
+ var metaDataTime = Assert.IsType<TimeSpan>(metadata);
+ Assert.Equal(options.ReplenishmentPeriod.Ticks * 2, metaDataTime.Ticks);
+
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata));
+ Assert.Equal(options.ReplenishmentPeriod.Ticks * 2, typedMetadata.Ticks);
+ Assert.Collection(failedLease.MetadataNames, item => item.Equals(MetadataName.RetryAfter.Name));
+ }
+
+ [Fact]
+ public async Task CorrectRetryMetadataWithQueuedItem()
+ {
+ var options = new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(20), 1, autoReplenishment: false);
+ var limiter = new TokenBucketRateLimiter(options);
+
+ using var lease = limiter.Acquire(2);
+ // Queue item which changes the retry after time for failed items
+ var wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ var failedLease = await limiter.WaitAsync(2);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata));
+ Assert.Equal(options.ReplenishmentPeriod.Ticks * 3, typedMetadata.Ticks);
+ }
+
+ [Fact]
+ public async Task CorrectRetryMetadataWithMultipleTokensPerPeriod()
+ {
+ var options = new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(20), 2, autoReplenishment: false);
+ var limiter = new TokenBucketRateLimiter(options);
+
+ using var lease = limiter.Acquire(2);
+ // Queue item which changes the retry after time for failed waits
+ var wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ var failedLease = await limiter.WaitAsync(2);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata));
+ Assert.Equal(options.ReplenishmentPeriod, typedMetadata);
+ }
+
+ [Fact]
+ public async Task CorrectRetryMetadataWithLargeTokensPerPeriod()
+ {
+ var options = new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(20), 100, autoReplenishment: false);
+ var limiter = new TokenBucketRateLimiter(options);
+
+ using var lease = limiter.Acquire(2);
+ // Queue item which changes the retry after time for failed items
+ var wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ var failedLease = await limiter.WaitAsync(2);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata));
+ Assert.Equal(options.ReplenishmentPeriod, typedMetadata);
+ }
+
+ [Fact]
+ public async Task CorrectRetryMetadataWithNonZeroAvailableItems()
+ {
+ var options = new TokenBucketRateLimiterOptions(3, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(20), 1, autoReplenishment: false);
+ var limiter = new TokenBucketRateLimiter(options);
+
+ using var lease = limiter.Acquire(2);
+
+ var failedLease = await limiter.WaitAsync(3);
+ Assert.False(failedLease.IsAcquired);
+ Assert.True(failedLease.TryGetMetadata(MetadataName.RetryAfter, out var typedMetadata));
+ Assert.Equal(options.ReplenishmentPeriod.Ticks * 2, typedMetadata.Ticks);
+ }
+
+ [Fact]
+ public void TryReplenishHonorsTokensPerPeriod()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(7, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 3, autoReplenishment: false));
+ Assert.True(limiter.Acquire(5).IsAcquired);
+ Assert.False(limiter.Acquire(3).IsAcquired);
+
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ Assert.True(limiter.TryReplenish());
+ Assert.Equal(5, limiter.GetAvailablePermits());
+
+ Assert.True(limiter.TryReplenish());
+ Assert.Equal(7, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public void TryReplenishWithAllTokensAvailable_Noops()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.Zero, 1, autoReplenishment: false));
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ Assert.True(limiter.TryReplenish());
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public void TryReplenishWithAutoReplenish_ReturnsFalse()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromSeconds(1), 1, autoReplenishment: true));
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ Assert.False(limiter.TryReplenish());
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ }
+
+ [Fact]
+ public async Task AutoReplenish_ReplenishesTokens()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 1,
+ TimeSpan.FromMilliseconds(1000), 1, autoReplenishment: true));
+ Assert.Equal(2, limiter.GetAvailablePermits());
+ limiter.Acquire(2);
+
+ var lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 2,
+ TimeSpan.Zero, 2, autoReplenishment: false));
+
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(2);
+ Assert.False(wait.IsCompleted);
+
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ lease = await limiter.WaitAsync(1);
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait.IsCompleted);
+
+ limiter.TryReplenish();
+
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3,
+ TimeSpan.Zero, 2, autoReplenishment: false));
+
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(2);
+ var wait2 = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+ Assert.False(wait2.IsCompleted);
+
+ limiter.TryReplenish();
+
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait2.IsCompleted);
+
+ limiter.TryReplenish();
+
+ lease = await wait2;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.NewestFirst, 3,
+ TimeSpan.Zero, 2, autoReplenishment: false));
+
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(2);
+ Assert.False(wait.IsCompleted);
+
+ lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+ Assert.False(wait.IsCompleted);
+
+ limiter.TryReplenish();
+
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public override async Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 3,
+ TimeSpan.Zero, 2, autoReplenishment: false));
+
+ var lease = limiter.Acquire(1);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(2);
+ Assert.False(wait.IsCompleted);
+
+ lease = limiter.Acquire(1);
+ Assert.False(lease.IsAcquired);
+
+ limiter.TryReplenish();
+
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+ }
+
+ [Fact]
+ public async Task ReplenishWorksWhenTicksWrap()
+ {
+ var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(10, QueueProcessingOrder.OldestFirst, 2,
+ TimeSpan.FromMilliseconds(2), 1, autoReplenishment: false));
+
+ var lease = limiter.Acquire(10);
+ Assert.True(lease.IsAcquired);
+
+ var wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ var replenishInternalMethod = typeof(TokenBucketRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!;
+ // This will set the last tick to the max value
+ replenishInternalMethod.Invoke(limiter, new object[] { uint.MaxValue });
+
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+
+ wait = limiter.WaitAsync(1);
+ Assert.False(wait.IsCompleted);
+
+ // ticks wrapped, should replenish
+ replenishInternalMethod.Invoke(limiter, new object[] { 2U });
+ lease = await wait;
+ Assert.True(lease.IsAcquired);
+
+ replenishInternalMethod.Invoke(limiter, new object[] { uint.MaxValue });
+
+ wait = limiter.WaitAsync(2);
+ Assert.False(wait.IsCompleted);
+
+ // ticks wrapped, but only 1 millisecond passed, make sure the wrapping behaves correctly and replenish doesn't happen
+ replenishInternalMethod.Invoke(limiter, new object[] { 1U });
+ Assert.False(wait.IsCompleted);
+ Assert.Equal(1, limiter.GetAvailablePermits());
+ }
+ }
+}