-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RateLimiting APIs #61788
Add RateLimiting APIs #61788
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @mangod9 Issue DetailsPart of dotnet/aspnetcore#37385 There were a couple minor code fixups needed for building in the repo because the project now targets more TFMs than just netstandard2.0. I will call out the changes in code review comments. Couple things to note:
|
} | ||
|
||
TaskCompletionSource<RateLimitLease> tcs = new TaskCompletionSource<RateLimitLease>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
CancellationTokenRegistration ctr = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added = default;
since the compiler doesn't like you using non-initialized variables.
{ | ||
ctr = cancellationToken.Register(obj => | ||
{ | ||
((TaskCompletionSource<RateLimitLease>)obj!).TrySetException(new OperationCanceledException(cancellationToken)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added !
to obj
, to fix nullable warning
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of cancellationToken
in the delegate is causing this method to always allocate a closure, even on the fast paths.
} | ||
|
||
/// <inheritdoc/> | ||
public bool Equals(MetadataName<T>? other) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added ?
since IEquatable
requires it, and added the if (other is null)
check
|
||
TaskCompletionSource<RateLimitLease> tcs = new TaskCompletionSource<RateLimitLease>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
||
CancellationTokenRegistration ctr = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added = default;
{ | ||
ctr = cancellationToken.Register(obj => | ||
{ | ||
((TaskCompletionSource<RateLimitLease>)obj!).TrySetException(new OperationCanceledException(cancellationToken)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added !
to obj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue here with regards to a closure.
} | ||
|
||
// Used in tests that test behavior with specific time intervals | ||
private void ReplenishInternal(uint nowTicks) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to private and use reflection in tests to avoid IVT
var wait = limiter.WaitAsync(1); | ||
Assert.False(wait.IsCompleted); | ||
|
||
var replenishInternalMethod = typeof(TokenBucketRateLimiter).GetMethod("ReplenishInternal", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to reflection to call private method instead of IVT
} | ||
|
||
// Perf: Check SemaphoreSlim implementation instead of locking | ||
if (_permitCount >= permitCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These non-synchronized fast-path checks mean we might return failure even if success is possible. That's ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine, it's an implicit race, locking would still race with someone releasing permits at the same time.
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/Internal/Deque.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/MetadataName.T.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/MetadataName.T.cs
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitLease.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitLease.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitLease.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.csproj
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/tests/System.Threading.RateLimiting.Tests.csproj
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/MetadataName.T.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/tests/Internal/TaskExtensions.cs
Outdated
Show resolved
Hide resolved
} | ||
public enum QueueProcessingOrder | ||
{ | ||
OldestFirst = 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approved API called these:
ProcessOldest,
ProcessNewest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approved API code was wrong, the comment above the code states:
We decided to rename the QueueProcessingOrder members from { ProcessOldest, ProcessNewest } to { OldestFirst, NewestFirst }
protected abstract 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; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't metadata be attributed with [MaybeNullWhen(false)]
?
What does it mean to return true
from TryGetMetadata
, but the metadata still being null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During one of the reviews it was discussed that a null value could be valid metadata
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember discussing wanting to distinguish between metadata that is sometimes defined but not present (returns true but gives null) vs metadata that is never present (returns false).
Previous discussion: #52079 (comment)
src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimitLease.cs
Show resolved
Hide resolved
public QueueProcessingOrder QueueProcessingOrder { get; } | ||
|
||
/// <summary> | ||
/// Maximum cumulative token count of queued acquisition requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description is different than ConcurrencyLimiterOptions:
/// Maximum number of permits that can be queued concurrently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done on purpose. The API review called out that TokenBucket should refer to permits as tokens where possible.
|
||
namespace System.Threading.RateLimiting | ||
{ | ||
public sealed partial class ConcurrencyLimiter : System.Threading.RateLimiting.RateLimiter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it odd that we have RateLimiter
, TokenBucketRateLimiter
, but then ConcurrencyLimiter
? Why not ConcurrencyRateLimiter
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've had a rate-limiting prototype sitting around in Polly for a while (App-vNext/Polly#666) so I thought I'd take a look at this - looks nice.
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Show resolved
Hide resolved
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
...ries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/QueueProcessingOrder.cs
Outdated
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Show resolved
Hide resolved
...em.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs
Outdated
Show resolved
Hide resolved
...em.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs
Outdated
Show resolved
Hide resolved
2010f64
to
f59ff02
Compare
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Show resolved
Hide resolved
...es/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/ConcurrencyLimiter.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Threading.RateLimiting/ref/System.Threading.RateLimiting.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
// Return SuccessfulLease or FailedLease to indicate limiter state | ||
if (permitCount == 0 && !_disposed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to throw ObjectDisposedException if this has been disposed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should an ODE throw synchronously or be wrapped in a ValueTask?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the first question is:
- Do we want to throw ODE or always return "failed" when a limiter is disposed?
Doing a quick check of other System.Threading disposable types, they seem to throw ODE: Barrier, ManualResetEventSlim, ReaderWriterLockSlim
For the next question, I would say it is similar to argument checking - so we would throw inline.
Part of dotnet/aspnetcore#37385
Moves the current non-generic APIs and two rate limiter implementations from https://github.com/aspnet/AspLabs/tree/main/src/RateLimiting.
There were a couple minor code fixups needed for building in the repo because the project now targets more TFMs than just netstandard2.0. I will call out the changes in code review comments.
Couple things to note:
TaskExtensions.DefaultTimeout()
methods from asplabs/aspnetcore that we use in our tests to detect hangs/prevent hangs on the CI. Not sure how the Runtime handles test hangs as I haven't seen tests do anything special with awaiting tasks.<IsPackable>
or any other metadata?