Open MadL1me opened 1 year ago
Related to #45652
Risk
IRateLimiterContextFeature - Consider making all of the properties read only. There's no scenario for replacing them, correct?
Yep, I think making them read only would be best way to do. At firstly, I thought we can provide more flexibility by not restricting it, but there is really no way to use setters in any adequate scenario
Risk
- would anyone use this to dispose of the lease early? Would that have any strange side-effects, especially if it got double-disposed by the middleware later?
Investigated dotnet/runtime. All limiters there correctly implement IDisposable, and can't be disposed twice:
Your point about disposing is right - yes, someone can dispose it later in middleware. I don't know about any scenarios how that can be used, all my thought about use case of this - add some ("admin"/"user" filtering logic - something we can already do with partitioned rate limiter)
After more thought, I think we can do this:
namespace Microsoft.AspNetCore.RateLimiting.Features;
public interface IRateLimiterContextFeature
{
HttpContext HttpContext { get; }
RateLimitLeaseInfo LeaseInfo { get; }
PartitionedRateLimiter<HttpContext>? GlobalLimiter { get; }
PartitionedRateLimiter<HttpContext> EndpointLimiter { get; }
}
public abstract class RateLimitLeaseInfo
{
public abstract bool IsAcquired { get; }
public abstract bool TryGetMetadata(string metadataName, out object? metadata);
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;
}
public abstract IEnumerable<string> MetadataNames { get; }
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);
}
}
}
}
There is 2 important changes:
@Tratcher is there anything I can research/investigate for this issue to continue in discussing? I think #44140 is pretty wanted as feature set
We'll review the API in a week or two when people are back from the holidays.
Let's imagine the statistics are stored in Redis/SQL, I wouldn't want to call GetStatistics() on every http request
Having HttpContext on a Feature interface is weirdly circular, normally you're retrieving features from the HttpContext. Why would you need to store the context?
GetStatistics() require context as parameter, because limiters in middleware are PartitionedLimiter
Also, I added alternative design in first message: https://github.com/dotnet/aspnetcore/issues/45658#issue-1501829704
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
How important is it to expose the global and endpoint PartitionedRateLimiter<HttpContext>
instances directly? Do we expect anyone will want to manually acquire additional leases with these? If that's something people really want to do, we might be able to expose both of these instances as services instead of features. Both PartitionedRateLimiter
instances are basically singletons, although you could have multiple if you're using multiple instances of the rate limiting middleware.
If acquiring additional leases is not a goal, it might be cleaner to expose RateLimiterStatistics? GetGlobalStatistics()
and RateLimiterStatistics? GetEndpointStatistics()
methods on the feature directly. That way we wouldn't have to worry about flowing the HttpContext
. Do you have any thoughts on this @BrennanConroy ?
API Review Notes:
PartitionedRateLimiter<HttpContext>
instances in a singleton service might help with getting the limiters outside a request context, but you still need an HttpContext to call GetStatistics. Could we parameters the EndpointRateLimiter on Endpoint instead of HttpContext?
PartitionedRateLimiter<Endpoint>
instead of PartitionedRateLimiter<HttpContext>
.HttpContext
because there could be a completely custom partitioning scheme.RateLimitLeaseInfo
in Alternative Design - 1? We don't want people calling Dispose() on it themselves, but we're not sure this warrants a mostly redundant type.IRateLimiterContextFeature.HttpContext
property? It could be convenient to save passing around an extra object sometimes, but it seem unnecessary. It could always be added later if it's too difficult to use without it.PartitionRateLimiter
level, but MemoryCache
has an opt-in API to avoid perf regressions. (https://github.com/dotnet/runtime/issues/50406#issuecomment-1074521694). We could pool IRateLimiterContextFeature
if we really want to.
GetStatistics()
APIs rather than expose the entire PartitionedRateLimiter<HttpContext>
yet. If it's the global one, it should be accessible to the user because they set it on the options.IRateLimitLeaseFeature
or IRateLimiterLeaseFeature
? Let's stick with "RateLimiter" even though it doesn't appear in the RateLimitLease
type.IRateLimiterLeaseFeature.Lease
be nullable? Endpoint
is nullable for the endpoint feature. And there won't be any lease if you disable rate limiting with the attribute, so yes.The following version the the API is approved!
+ namespace Microsoft.AspNetCore.RateLimiting.Features;
+ public interface IRateLimiterStatisticsFeature
+ {
+ RateLimiterStatistics? GetGlobalStatistics();
+ RateLimiterStatistics? GetEndpointStatistics();
+ }
+ public interface IRateLimiterLeaseFeature
+ {
+ RateLimitLease? Lease { get; }
+ }
namespace Microsoft.AspNetCore.RateLimiting;
public sealed class RateLimiterOptions
{
// IRateLimiterStatisticsFeature won't be set unless this is opted into, but will not be unset after the rate limiting middleware exits.
// IRateLimiterLeaseFeature will always be set, but we should be able to pool it since we unset to avoid exposing a disposed lease.
+ public bool TrackStatistics { get; set; }
}
@halter73 thank you for review, and sorry if my proposal wasted a lot of time to rethinking my design. I'll try to learn from your thought process, and I hope I'll make better API proposals next time
@MadL1me Your proposal was really good and got us thinking in the right direction. Thanks!
And the API review notes are not from me specifically. I'm summarizing feedback from a bunch of engineers who discussed this in an API review meeting. I don't think any one of us would have come up with this exact design initially. It's a process.
@MadL1me Will you be updating your PR (https://github.com/dotnet/aspnetcore/pull/45652/files) based on this API review feedback?
Yes! I have an exam in 2 days, after that I'll submit a PR @adityamandaleeka
Decided to make proposed this API with 2 separate PR's for each feature, so I'm closed this old PR (https://github.com/dotnet/aspnetcore/pull/45652). All work is moved to https://github.com/dotnet/aspnetcore/pull/46028
Thanks for contacting us.
We're moving this issue to the .NET 8 Planning
milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.
@MadL1me Thanks, and feel free to ignore the bot message above. Look forward to seeing your PRs go in!
+ namespace Microsoft.AspNetCore.RateLimiting.Features;
@halter73 why did we decide to put this in the Features namespace? That's not something we've done for other components like https://github.com/dotnet/aspnetcore/blob/334da01db159058defbd39f128d659ddf3ae3f7e/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs#L4-L9 https://github.com/dotnet/aspnetcore/blob/334da01db159058defbd39f128d659ddf3ae3f7e/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerFeature.cs#L7-L12
Agreed
I think we added the namespace because most of our HTTP and connection-level features live in Features namespaces, and we wanted to be consistent. However, I agree we should move the new features middleware's primary namespace considering we've already done that for output caching and exception handler middleware. It's nice to have less namespaces, and it's not like it's too cluttered.
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
@halter73 in your message (https://github.com/dotnet/aspnetcore/issues/45658#issuecomment-1372967290) you wrote:
IRateLimiterLeaseFeature will always be set, but we should be able to pool it since we unset to avoid exposing a disposed lease.
Can you please clarify what that means? How is object pooling corresponds with avoiding exposing disposed lease?
Can you please clarify what that means? How is object pooling corresponds with avoiding exposing disposed lease?
I wouldn't do this as part of the initial change. But if we wanted to later, we could return the object to the pool in the rate limiting middleware after we unset the feature. We do not want to pool anything that is still accessible via the HttpContext.
Thinking about this a little, the GetEndpointStatistics
and GetGlobalStatistics
methods imply getting statistics for the specific Endpoint and for the whole app respectively. But in reality, they are getting statistics for a specific request type at the app level and at the Endpoint
level.
What that means is that the statistics can be very different per request even if the requests end up on the same Endpoint
. For example, if you partition based on User
or query string you'll get a completely different limiter, even though the request goes to the same Endpoint
, which means when you call one of the statistics methods you will get completely different statistics.
The method naming/usage does little to suggest this behavior which I would argue is going to be confusing to users.
That is a good point about the naming not indicating that the statistics are arbitrarily partitioned based on how the policies and/or global limiters are configured.
Do you have any suggestions for names that would make this clearer? Maybe something like GetCurrentPartitionStatistics()
? The downside is that you couldn't query just the global or endpoint statistics for the given request, but I think that might be okay.
+ namespace Microsoft.AspNetCore.RateLimiting;
+ public interface IRateLimiterStatisticsFeature
+ {
+ RateLimiterMiddlewareStatistics GetCurrentPartitionStatistics();
+ }
+ public sealed class RateLimiterMiddlewareStatistics
+ {
+ RateLimiterStatistics? GlobalStatistics { get; init; }
+ RateLimiterStatistics? EndpointStatistics { get; init; }
+ }
API Review Notes:
Microsoft.AspNetCore.RateLimiting.Features
namespace.IRateLimiterStatisticsFeature
only referencing the partition of the current request. We think it's fine to do that, but it might not be obvious enough what's really going on given the current API naming.We will try to get back to this in API review Monday. I'm leaving it to @BrennanConroy to improve the proposal since he doesn't like my proposal 😆 .
Thinking about this a little, the GetEndpointStatistics and GetGlobalStatistics methods imply getting statistics for the specific Endpoint and for the whole app respectively. But in reality, they are getting statistics for a specific request type at the app level and at the Endpoint level.
What if we try to do it that way? For example, we could return a List
of statistics, because we know that Middleware always uses PartitionedRateLimiter
:
public sealed class RateLimiterMiddlewareStatistics
{
// new props, contain statistics for ALL limiters in PartitonedRateLimiter
List<RateLimiterStatistics?> GlobalStatistics { get; init; }
List<RateLimiterStatistics?> EndpointStatistics { get; init; }
// props proposed before, renamed to indicate that partition is used
RateLimiterStatistics? PartitionGlobalStatistics{ get; init; }
RateLimiterStatistics? PartitionEndpointStatistics { get; init; }
}
However, this probably would require add methods to fetch all statistics from PartitionedRateLimiter
.
@halter73 bubbling this to the top of your stack. See issue #47456 where a scenario involving accessing the rate limiter from the feature came up. Basically the idea is that in addition to getting a ticket to execute the endpoint, the developer might decide to eat up some additional tickets after execution IF the request warranted it.
Sometimes it's hard to know whether a given request will be expensive in advance, so deducting the tickets after execution.
@halter73 @mitchdenny I think we should combine statistics solution with #47456 idea.
If we create and expose entire IRateLimiterFeature
, it'll make possible for a custom RateLimiterMiddleware
to be made, which we can use with #47456 scenario as well as "fetching Statistics" problem.
It looks like this hasn't been implemented yet. I'd like to revisit the conversation in light of the following issue:
https://github.com/dotnet/aspnetcore/issues/47456
This issue illustrates a scenario where being able to access the global/endpoint limiters via a feature inside some middleware could be useful. For example you might use rate limiters to automatically consume a token for every request, but after a request has been processed you might decide that particular request was more expensive so you want to take more tokens.
If we exposed the rate limiters via a feature this could be easily implemented. Even though this API proposal was more about statistics, by exposing the rate limiters we would allow both scenarios.
/cc @halter73
@mitchdenny agreed, I think exposing limiters via features would add a great flexibility to feature usage. I would like to continue to work on this issue, after .net team discussion.
It seems like some parts of this proposal are still stuck in the discussion phase. Personally I am not that interested in the IRateLimiterStatisticsFeature
portion right now but in accessing the current lease using the proposed IRateLimiterLeaseFeature
.
In https://github.com/cristipufu/aspnetcore-redis-rate-limiting/wiki/Rate-Limiting-Headers when acquiring the lease remaining/limit/reset information is cheaply available, stored in the lease and made available as metadata on the lease. Having IRateLimiterLeaseFeature
would solve the initial idea behind #44140 on its own for these rate limiters and others using a similar approach. As far as I can tell it is also the only approach to return exact values from the time of acquire if they are available. It would of course also unlock any other use-case that can be built based on the lease information on its own.
@BrennanConroy (or whoever can decide this): Is it possible to approve and pursue IRateLimiterLeaseFeature
independently of the statistics specific parts of the proposal?
Independent of my previous question I have some thoughts on the statistics portion of the proposal: RateLimiter.GetStatistics is a blocking method without any specific guidance on how it should behave.
If the expectation is to be able to readily call it for every request to populate RateLimiterStatistics fields this has to be clarified as that is not easily achieved in distributed scenarios. E.g. just doing a redis query in there as https://github.com/cristipufu/aspnetcore-redis-rate-limiting/ does will lead to thread pool exhaustion with a bit of load. https://github.com/dotnet/runtime/issues/88592 proposes an async variant of GetStatistics
but it hasn't progressed yet and it might still be an expensive call.
If the expectation is to do updates in the background around the calls as hinted at in the review discussion from Jan 6th 23 I am unsure how that could be achieved generically in an efficient way for a heavily partitioned rate limiter. It could always serve stale data from the last call and schedule a refresh after but that might get very stale and will not work for the first call. Doing regular updates outside of that would retrieve a lot of statistics for rate limiters you might not end up needing for any request in that timeframe. Also bound to be a bit stale but more predictable.
The rate limiter itself might be able to get up-to-date statistics and cache them for GetStatistics
calls when a lease is acquired. Where possible that would give the best results for this proposal and #44140. But then what about other uses of GetStatistics
outside of a specific request context that expect to get a current snapshot?
TL;DR: Imo behavior of RateLimiter.GetStatistics has to be clarified before it is possible to say whether it can be safely and efficiently used to retrieve statistics here.
Came across this issue while googling and I'll just add on here that I've been experimenting with consuming a variable amount of tokens, but also to manually acquire leases and expect the statistics. So I'd very much like a nice way to get access to active limiters.
Background and Motivation
In https://github.com/dotnet/aspnetcore/issues/44140 issue, one of The dream scenario for a better ASP.NET Core rate limiter middleware as @maartenba says, would be able Have a feature on the current HttpContext that gives access to the current rate limit context, so these details can also be returned on successful requests, or added to telemetry.. This API proposal addresses this concern.
Proposed API
Below API supposes, that we have
RateLimiterMiddleware
in our app, which responsibility is to createIRateLimiterContextFeature
for each request:Now we can access to this interface in any custom middleware or controller:
Usage Examples
Scenario 1: get statistics from limiters in custom telemetry middleware:
Scenario 2: Get metadata from successful RateLimiterLease for this request
Alternative Design - 1
As @Tratcher said, there is a risk, that
RateLimitLease
would be disposed early. To prevent that, we can introduce facade-like class to wrap Lease with undisposable entity:Alternative Design - 2
Also, Instead of using
HttpContext
Features, we can useHttpContext.Items
and Extension methods, like its done inMicrosoft.AspNetCore.Authentication
extention methods (HttpContext.SingIn
,HttpContext.ChallangeAsync
, etc). So, we use the Alternative Design - 1 approach with Facade-like api, but implemented with extension methodsRisks
In Design 1: