Skip to content
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

[API Proposal]: Improve RateLimiter metrics #71804

Closed
BrennanConroy opened this issue Jul 8, 2022 · 2 comments · Fixed by #72306
Closed

[API Proposal]: Improve RateLimiter metrics #71804

BrennanConroy opened this issue Jul 8, 2022 · 2 comments · Fixed by #72306
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading
Milestone

Comments

@BrennanConroy
Copy link
Member

Background and motivation

One of the original potential use cases cited for RateLimiter.GetAvailablePermits() was for diagnostics. While it's a good starting point, it would make more sense to include more information to include in diagnostics. So, taking inspiration from MemoryCache we are proposing changing GetAvailablePermits to a more useful API: GetStatistics.

API Proposal

namespace System.Threading.RateLimiting;

public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits();
+    public abstract RateLimiterStatistics? GetStatistics();
}

public abstract class PartitionedRateLimiter<TResource> : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits(TResource resourceID);
+    public abstract RateLimiterStatistics? GetStatistics(TResource resourceID);
}

+ public class RateLimiterStatistics
+ {
+    public RateLimiterStatistics();

+    public long CurrentAvailablePermits { get; init; }

+    public long CurrentQueuedCount { get; init; }

+    public long TotalFailedLeases { get; init; }

+    public long TotalSuccessfulLeases { get; init; }
+ }

API Usage

Meter meter = new Meter("RateLimiter", "1.0.0");
RateLimiter limiter = new ConcurrencyLimiter(
    new ConcurrencyLimiterOptions(100, QueueProcessingOrder.OldestFirst, 20));

meter.CreateObservableGauge<long>("available-permits", GetAvailablePermits);

IEnumerable<Measurement<long>> GetAvailablePermits()
{
    return new Measurement<long>[]
    {
        new Measurement<long>(limiter.GetStatistics()!.CurrentAvailablePermits,
            new KeyValuePair<string, object?>("Limiter", "Concurrent")),
    };
}

Alternative Designs

An alternative name is GetCurrentStatistics which is what MemoryCache uses.

Another property that might be useful to include is long TotalAcquiredPermits { get; init; }

There has been discussion around allowing rate limiter specific state to be returned, either via this API or another API. This would be similar to HttpClientHandler.Properties

public IDictionary<string, object?> Properties => _underlyingHandler.Properties;

Two ways to allow state without introducing additional APIs to what is already proposed is to use the existing metadata on RateLimitLease and call Acquire with 0 permits to always get back a lease without acquiring any permits. Or we keep the new RateLimiterStatistics class that's being proposed as non-sealed and allow implementations to provide their own statistics type that contains custom state.

Risks

If someone was wanting to query GetAvailablePermits before every call to Acquire or WaitAsync they would now need to call a more expensive method (allocates every call) to get the same behavior. I don't believe this would be a common pattern, if someone wants to acquire permits quickly without potential waiting, they can call Acquire which will already check for availability and be more accurate than calling GetAvailablePermits then Acquire if there are enough permits.

@BrennanConroy BrennanConroy added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading labels Jul 8, 2022
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jul 8, 2022
@ghost
Copy link

ghost commented Jul 8, 2022

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

One of the original potential use cases cited for RateLimiter.GetAvailablePermits() was for diagnostics. While it's a good starting point, it would make more sense to include more information to include in diagnostics. So, taking inspiration from MemoryCache we are proposing changing GetAvailablePermits to a more useful API: GetStatistics.

API Proposal

namespace System.Threading.RateLimiting;

public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits();
+    public abstract RateLimiterStatistics? GetStatistics();
}

public abstract class PartitionedRateLimiter<TResource> : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits(TResource resourceID);
+    public abstract RateLimiterStatistics? GetStatistics(TResource resourceID);
}

+ public class RateLimiterStatistics
+ {
+    public RateLimiterStatistics();

+    public long CurrentAvailablePermits { get; init; }

+    public long CurrentQueuedCount { get; init; }

+    public long TotalFailedLeases { get; init; }

+    public long TotalSuccessfulLeases { get; init; }
+ }

API Usage

Meter meter = new Meter("RateLimiter", "1.0.0");
RateLimiter limiter = new ConcurrencyLimiter(
    new ConcurrencyLimiterOptions(100, QueueProcessingOrder.OldestFirst, 20));

meter.CreateObservableGauge<long>("available-permits", GetAvailablePermits);

IEnumerable<Measurement<long>> GetAvailablePermits()
{
    return new Measurement<long>[]
    {
        new Measurement<long>(limiter.GetStatistics()!.CurrentAvailablePermits,
            new KeyValuePair<string, object?>("Limiter", "Concurrent")),
    };
}

Alternative Designs

An alternative name is GetCurrentStatistics which is what MemoryCache uses.

Another property that might be useful to include is long TotalAcquiredPermits { get; init; }

There has been discussion around allowing rate limiter specific state to be returned, either via this API or another API. This would be similar to HttpClientHandler.Properties

public IDictionary<string, object?> Properties => _underlyingHandler.Properties;

Two ways to allow state without introducing additional APIs to what is already proposed is to use the existing metadata on RateLimitLease and call Acquire with 0 permits to always get back a lease without acquiring any permits. Or we keep the new RateLimiterStatistics class that's being proposed as non-sealed and allow implementations to provide their own statistics type that contains custom state.

Risks

If someone was wanting to query GetAvailablePermits before every call to Acquire or WaitAsync they would now need to call a more expensive method (allocates every call) to get the same behavior. I don't believe this would be a common pattern, if someone wants to acquire permits quickly without potential waiting, they can call Acquire which will already check for availability and be more accurate than calling GetAvailablePermits then Acquire if there are enough permits.

Author: BrennanConroy
Assignees: -
Labels:

api-suggestion, area-System.Threading

Milestone: -

@mangod9 mangod9 removed the untriaged New issue has not been triaged by the area owner label Jul 12, 2022
@mangod9 mangod9 added this to the 7.0.0 milestone Jul 12, 2022
@BrennanConroy BrennanConroy added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jul 12, 2022
@bartonjs
Copy link
Member

bartonjs commented Jul 14, 2022

Video

Looks good as proposed. (Changed resourceID to resource to match an in-flight rename request)

namespace System.Threading.RateLimiting;

public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits();
+    public abstract RateLimiterStatistics? GetStatistics();
}

public abstract class PartitionedRateLimiter<TResource> : IAsyncDisposable, IDisposable
{
-    public abstract int GetAvailablePermits(TResource resourceID);
+    public abstract RateLimiterStatistics? GetStatistics(TResource resource);
}

+ public class RateLimiterStatistics
+ {
+    public RateLimiterStatistics();

+    public long CurrentAvailablePermits { get; init; }

+    public long CurrentQueuedCount { get; init; }

+    public long TotalFailedLeases { get; init; }

+    public long TotalSuccessfulLeases { get; init; }
+ }

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 14, 2022
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 16, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Aug 12, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Sep 11, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Threading
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants