-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Partitioned (Design Generic) Rate Limiting APIs #37383
Comments
The proposed API for generic rate limiters will follow the API for non-generic rate limiters, because it keeps the rate limiting APIs aligned and currently we don't see any use cases that should cause the API to differ yet. public abstract class GenericRateLimiter<TResource> : IAsyncDisposable, IDisposable
{
public abstract int GetAvailablePermits(TResource resourceID);
public RateLimitLease Acquire(TResource resourceID, int permitCount = 1);
protected abstract RateLimitLease AcquireCore(TResource resourceID, int permitCount);
public ValueTask<RateLimitLease> WaitAsync(TResource resourceID, int permitCount = 1, CancellationToken cancellationToken = default);
protected abstract ValueTask<RateLimitLease> WaitAsyncCore(TResource resourceID, int permitCount, CancellationToken cancellationToken);
protected virtual void Dispose(bool disposing) { }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual ValueTask DisposeAsyncCore()
{
return default;
}
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore().ConfigureAwait(false);
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
} What is more interesting IMO is the potential implementations of an private readonly ConcurrentDictionary<string, RateLimiter> _limiters = new();
private readonly RateLimiter _defaultLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.FromSeconds(1), 1, true));
private RateLimiter GetRateLimiter(HttpRequestMessage resource)
{
if (!_limiters.TryGetValue(resource.RequestUri.AbsolutePath, out var limiter))
{
if (resource.RequestUri.AbsolutePath.StartsWith("/problem", StringComparison.OrdinalIgnoreCase))
{
limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1));
}
else
{
limiter = _defaultLimiter;
}
limiter = _limiters.GetOrAdd(resource.RequestUri.AbsolutePath, limiter);
}
return limiter;
} The above starts showing some of the complexities of implementing an
And there are additional non-obvious concerns:
To make public class GenericRateLimitBuilder<TResource>
{
public GenericRateLimitBuilder<TResource> WithPolicy<TKey>(Func<TResource, TKey?> keyFactory, Func<TKey, RateLimiter> limiterFactory) where TKey : notnull;
public GenericRateLimitBuilder<TResource> WithConcurrencyPolicy<TKey>(Func<TResource, TKey?> keyFactory, ConcurrencyLimiterOptions options) where TKey : notnull;
// Assuming we have a ReplenishingRateLimiter limiter abstract class
// public GenericRateLimitBuilder<TResource> WithReplenishingPolicy(Func<TResource, TKey?> keyFactory, Func<TKey, ReplenishingRateLimiter> replenishingRateLimiter) where TKey : notnull;
public GenericRateLimitBuilder<TResource> WithTokenBucketPolicy<TKey>(Func<TResource, TKey?> keyFactory, TokenBucketRateLimiterOptions options) where TKey : notnull;
// might want this to be a factory if the builder is re-usable
public GenericRateLimitBuilder<TResource> WithDefaultRateLimiter(RateLimiter defaultRateLimiter);
public GenericRateLimiter<TResource> Build();
} Details:
Questions:
One scenario that isn't handled by the builder proposed above is the ability to combine rate limiters. Imagine you want a global limiter of 100 concurrent requests to a service and also to have a per IP limit of 1 per second. + static GenericRateLimiter<TResource> CreateChainedRateLimiter<TResource>(IEnumerable<GenericRateLimiter<TResource>> limiters);` Additionally, we would like to add an interface for rate limiters that refresh tokens to make it easier to handle replenishing tokens from a single timer in generic code + public interface IReplenishingRateLimiter
+ {
+ public abstract bool TryReplenish();
+ // public TimeSpan ReplenishRate { get; }
+ } public sealed class TokenBucketRateLimiter
: RateLimiter
+ , IReplenishingRateLimiter Alternatively we could use a new abstract class And finally, we would like to add an API for checking if a rate limiter is idle. This would be used to see which rate limiters are broadcasting that they aren't being used and we can potentially remove them from our public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
+ public abstract DateTime? IdleSince { get; }
// alternatives
// bool IsInactive { get; }
// bool IsIdle { get; }
} Alternatively we could also add an interface, Example usage: var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("RateLimited", o => o.BaseAddress = new Uri("http://localhost:5000"))
.AddHttpMessageHandler(() =>
new RateLimitedHandler(
new GenericRateLimitBuilder<HttpRequestMessage>()
// TokenBucketRateLimiter if the request is a POST
.WithTokenBucketPolicy(request => request.Method.Equals(HttpMethod.Post) ? HttpMethod.Post : null,
new TokenBucketRateLimiterOptions(1, QueueProcessingOrder.OldestFirst, 1, TimeSpan.FromSeconds(1), 1, true))
// ConcurrencyLimiter if above limiter returns null and has a "cookie" header
.WithPolicy(request => request.Headers.TryGetValues("cookie", out _) ? "cookie" : null,
_ => new ConcurrencyLimiter(new ConcurrencyLimiterOptions(1, QueueProcessingOrder.NewestFirst, 1)))
// ConcurrencyLimiter per unique URI
.WithConcurrencyPolicy(request => request.RequestUri,
new ConcurrencyLimiterOptions(2, QueueProcessingOrder.OldestFirst, 2))
.Build()));
// ...
var factory = app.Services.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient("RateLimited");
var resp = await client.GetAsync("/problem"); |
Having a public class AggregateRateLimitBuilder<TResource>
{
public AggregateRateLimitBuilder<TResource> WithNoPolicy<TKey>(Func<TResource, bool> condition);
} |
Here's the issue in the runtime repo tracking this work: dotnet/runtime#65400 |
The text was updated successfully, but these errors were encountered: