Skip to content

Commit 13b92c0

Browse files
committed
Update
1 parent 657987f commit 13b92c0

File tree

7 files changed

+276
-99
lines changed

7 files changed

+276
-99
lines changed

src/RateLimiting/src/ConcurrencyLimiter.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ protected override ValueTask<RateLimitLease> WaitAsyncCore(int permitCount, Canc
8282
throw new ArgumentOutOfRangeException(nameof(permitCount), $"{permitCount} permits exceeds the permit limit of {_options.PermitLimit}.");
8383
}
8484

85-
// Return SuccessfulAcquisition if requestedCount is 0 and resources are available
85+
// Return SuccessfulLease if requestedCount is 0 and resources are available
8686
if (permitCount == 0 && _permitCount > 0)
8787
{
8888
return new ValueTask<RateLimitLease>(SuccessfulLease);
@@ -192,14 +192,6 @@ private void Release(int releaseCount)
192192
}
193193
}
194194

195-
private void CancellationRequested(TaskCompletionSource<RateLimitLease> tcs, CancellationToken token)
196-
{
197-
lock (_lock)
198-
{
199-
tcs.TrySetException(new OperationCanceledException(token));
200-
}
201-
}
202-
203195
private class ConcurrencyLease : RateLimitLease
204196
{
205197
private bool _disposed;
@@ -270,8 +262,6 @@ public RequestRegistration(int requestedCount, TaskCompletionSource<RateLimitLea
270262
public TaskCompletionSource<RateLimitLease> Tcs { get; }
271263

272264
public CancellationTokenRegistration CancellationTokenRegistration { get; }
273-
274-
public CancellationTokenRegistration CancellationTokenRegistration { get; }
275265
}
276266
}
277267
}

src/RateLimiting/src/RateLimiter.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public abstract class RateLimiter
3131
/// <remarks>
3232
/// Set <paramref name="permitCount"/> to 0 to get whether permits are exhausted.
3333
/// </remarks>
34-
/// <param name="permitCount">Number of permits to try and acquire.</param>
3534
/// <returns>A successful or failed lease.</returns>
3635
/// <exception cref="ArgumentOutOfRangeException"></exception>
3736
public RateLimitLease Acquire(int permitCount = 1)

src/RateLimiting/src/TokenBucketRateLimiter.cs

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Collections.Generic;
77
using System.Diagnostics;
8+
using System.Diagnostics.CodeAnalysis;
89
using System.Threading.Tasks;
910

1011
namespace System.Threading.RateLimiting
@@ -19,16 +20,18 @@ public sealed class TokenBucketRateLimiter : RateLimiter
1920
private long _lastReplenishmentTick = Environment.TickCount;
2021

2122
private readonly Timer? _renewTimer;
22-
private readonly object _lock = new object();
2323
private readonly TokenBucketRateLimiterOptions _options;
2424
private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>();
2525

26+
// 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
27+
private object Lock => _queue;
28+
2629
private static readonly RateLimitLease SuccessfulLease = new TokenBucketLease(true, null);
2730

2831
/// <summary>
2932
/// Initializes the <see cref="TokenBucketRateLimiter"/>.
3033
/// </summary>
31-
/// <param name="options"></param>
34+
/// <param name="options">Options to specify the behavior of the <see cref="TokenBucketRateLimiter"/>.</param>
3235
public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
3336
{
3437
_tokenCount = options.TokenLimit;
@@ -40,10 +43,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
4043
}
4144
}
4245

43-
/// <summary>
44-
/// An estimated count of available tokens.
45-
/// </summary>
46-
/// <returns></returns>
46+
/// <inheritdoc/>
4747
public override int GetAvailablePermits() => _tokenCount;
4848

4949
/// <inheritdoc/>
@@ -52,26 +52,25 @@ protected override RateLimitLease AcquireCore(int tokenCount)
5252
// These amounts of resources can never be acquired
5353
if (tokenCount > _options.TokenLimit)
5454
{
55-
throw new InvalidOperationException($"{tokenCount} tokens exceeds the token limit of {_options.TokenLimit}.");
55+
throw new ArgumentOutOfRangeException(nameof(tokenCount), $"{tokenCount} tokens exceeds the token limit of {_options.TokenLimit}.");
5656
}
5757

58-
// Return SuccessfulAcquisition or FailedAcquisition depending to indicate limiter state
58+
// Return SuccessfulLease or FailedLease depending to indicate limiter state
5959
if (tokenCount == 0)
6060
{
61-
if (GetAvailablePermits() > 0)
61+
if (_tokenCount > 0)
6262
{
6363
return SuccessfulLease;
6464
}
6565

6666
return CreateFailedTokenLease(tokenCount);
6767
}
6868

69-
lock (_lock)
69+
lock (Lock)
7070
{
71-
if (GetAvailablePermits() >= tokenCount)
71+
if (TryLeaseUnsynchronized(tokenCount, out RateLimitLease? lease))
7272
{
73-
_tokenCount -= tokenCount;
74-
return SuccessfulLease;
73+
return lease;
7574
}
7675

7776
return CreateFailedTokenLease(tokenCount);
@@ -86,23 +85,20 @@ protected override ValueTask<RateLimitLease> WaitAsyncCore(int tokenCount, Cance
8685
// These amounts of resources can never be acquired
8786
if (tokenCount > _options.TokenLimit)
8887
{
89-
throw new InvalidOperationException($"{tokenCount} token(s) exceeds the permit limit of {_options.TokenLimit}.");
88+
throw new ArgumentOutOfRangeException(nameof(tokenCount), $"{tokenCount} token(s) exceeds the permit limit of {_options.TokenLimit}.");
9089
}
9190

9291
// Return SuccessfulAcquisition if requestedCount is 0 and resources are available
93-
if (tokenCount == 0 && GetAvailablePermits() > 0)
92+
if (tokenCount == 0 && _tokenCount > 0)
9493
{
95-
// Perf: static failed/successful value tasks?
9694
return new ValueTask<RateLimitLease>(SuccessfulLease);
9795
}
9896

99-
lock (_lock)
97+
lock (Lock)
10098
{
101-
if (GetAvailablePermits() >= tokenCount && GetAvailablePermits() != 0)
99+
if (TryLeaseUnsynchronized(tokenCount, out RateLimitLease? lease))
102100
{
103-
_tokenCount -= tokenCount;
104-
// Perf: static failed/successful value tasks?
105-
return new ValueTask<RateLimitLease>(SuccessfulLease);
101+
return new ValueTask<RateLimitLease>(lease);
106102
}
107103

108104
// Don't queue if queue limit reached
@@ -116,8 +112,12 @@ protected override ValueTask<RateLimitLease> WaitAsyncCore(int tokenCount, Cance
116112
CancellationTokenRegistration ctr;
117113
if (cancellationToken.CanBeCanceled)
118114
{
119-
ctr = cancellationToken.Register(obj => CancellationRequested((TaskCompletionSource<RateLimitLease>)obj, cancellationToken), tcs);
115+
ctr = cancellationToken.Register(obj =>
116+
{
117+
((TaskCompletionSource<RateLimitLease>)obj).TrySetException(new OperationCanceledException(cancellationToken));
118+
}, tcs);
120119
}
120+
121121
RequestRegistration registration = new RequestRegistration(tokenCount, tcs, ctr);
122122
_queue.EnqueueTail(registration);
123123
_queueCount += tokenCount;
@@ -130,12 +130,39 @@ protected override ValueTask<RateLimitLease> WaitAsyncCore(int tokenCount, Cance
130130

131131
private RateLimitLease CreateFailedTokenLease(int tokenCount)
132132
{
133-
int replenishAmount = tokenCount - GetAvailablePermits() + _queueCount;
133+
int replenishAmount = tokenCount - _tokenCount + _queueCount;
134134
// can't have 0 replenish periods, that would mean it should be a successful lease
135135
// if TokensPerPeriod is larger than the replenishAmount needed then it would be 0
136136
int replenishPeriods = Math.Max(replenishAmount / _options.TokensPerPeriod, 1);
137137

138-
return new TokenBucketLease(false, TimeSpan.FromTicks(_options.ReplenishmentPeriod.Ticks*replenishPeriods));
138+
return new TokenBucketLease(false, TimeSpan.FromTicks(_options.ReplenishmentPeriod.Ticks * replenishPeriods));
139+
}
140+
141+
private bool TryLeaseUnsynchronized(int tokenCount, [NotNullWhen(true)] out RateLimitLease? lease)
142+
{
143+
// if permitCount is 0 we want to queue it if there are no available permits
144+
if (_tokenCount >= tokenCount && _tokenCount != 0)
145+
{
146+
if (tokenCount == 0)
147+
{
148+
// Edge case where the check before the lock showed 0 available permits but when we got the lock some permits were now available
149+
lease = SuccessfulLease;
150+
return true;
151+
}
152+
153+
// a. if there are no items queued we can lease
154+
// 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
155+
if (_queueCount == 0 || (_queueCount > 0 && _options.QueueProcessingOrder == QueueProcessingOrder.NewestFirst))
156+
{
157+
_tokenCount -= tokenCount;
158+
Debug.Assert(_tokenCount >= 0);
159+
lease = SuccessfulLease;
160+
return true;
161+
}
162+
}
163+
164+
lease = null;
165+
return false;
139166
}
140167

141168
/// <summary>
@@ -165,7 +192,7 @@ private static void Replenish(object? state)
165192
long nowTicks = Environment.TickCount * TimeSpan.TicksPerMillisecond;
166193

167194
// method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes
168-
lock (limiter!._lock)
195+
lock (limiter!.Lock)
169196
{
170197
if (nowTicks - limiter._lastReplenishmentTick < limiter._options.ReplenishmentPeriod.Ticks)
171198
{
@@ -214,7 +241,6 @@ private static void Replenish(object? state)
214241
Debug.Assert(limiter._queueCount >= 0);
215242
Debug.Assert(limiter._tokenCount >= 0);
216243

217-
// requestToFulfill == request
218244
if (!nextPendingRequest.Tcs.TrySetResult(SuccessfulLease))
219245
{
220246
// Queued item was canceled so add count back
@@ -231,14 +257,6 @@ private static void Replenish(object? state)
231257
}
232258
}
233259

234-
private void CancellationRequested(TaskCompletionSource<RateLimitLease> tcs, CancellationToken token)
235-
{
236-
lock (_lock)
237-
{
238-
tcs.TrySetException(new OperationCanceledException(token));
239-
}
240-
}
241-
242260
private class TokenBucketLease : RateLimitLease
243261
{
244262
private readonly TimeSpan? _retryAfter;
@@ -255,12 +273,10 @@ public TokenBucketLease(bool isAcquired, TimeSpan? retryAfter)
255273

256274
private IEnumerable<string> Enumerable()
257275
{
258-
if (_retryAfter is null)
276+
if (_retryAfter is not null)
259277
{
260-
yield break;
278+
yield return MetadataName.RetryAfter.Name;
261279
}
262-
263-
yield return MetadataName.RetryAfter.Name;
264280
}
265281

266282
public override bool TryGetMetadata(string metadataName, out object? metadata)
@@ -271,14 +287,14 @@ public override bool TryGetMetadata(string metadataName, out object? metadata)
271287
return true;
272288
}
273289

274-
metadata = null;
290+
metadata = default;
275291
return false;
276292
}
277293

278294
protected override void Dispose(bool disposing) { }
279295
}
280296

281-
private struct RequestRegistration
297+
private readonly struct RequestRegistration
282298
{
283299
public RequestRegistration(int tokenCount, TaskCompletionSource<RateLimitLease> tcs, CancellationTokenRegistration cancellationTokenRegistration)
284300
{

src/RateLimiting/src/TokenBucketRateLimiterOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public TokenBucketRateLimiterOptions(
7878
/// Determines the behaviour of <see cref="RateLimiter.WaitAsync"/> when not enough resources can be leased.
7979
/// </summary>
8080
/// <value>
81-
/// <see cref="QueueProcessingOrder.OldestFirst"/>
81+
/// <see cref="QueueProcessingOrder.OldestFirst"/> by default.
8282
/// </value>
8383
public QueueProcessingOrder QueueProcessingOrder { get; }
8484

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
namespace System.Threading.RateLimiting.Test
8+
{
9+
public abstract class BaseRateLimiterTests
10+
{
11+
[Fact]
12+
public abstract void CanAcquireResource();
13+
14+
[Fact]
15+
public abstract void InvalidOptionsThrows();
16+
17+
[Fact]
18+
public abstract Task CanAcquireResourceAsync();
19+
20+
[Fact]
21+
public abstract Task CanAcquireResourceAsync_QueuesAndGrabsOldest();
22+
23+
[Fact]
24+
public abstract Task CanAcquireResourceAsync_QueuesAndGrabsNewest();
25+
26+
[Fact]
27+
public abstract Task FailsWhenQueuingMoreThanLimit();
28+
29+
[Fact]
30+
public abstract Task QueueAvailableAfterQueueLimitHitAndResources_BecomeAvailable();
31+
32+
[Fact]
33+
public abstract void ThrowsWhenAcquiringMoreThanLimit();
34+
35+
[Fact]
36+
public abstract Task ThrowsWhenWaitingForMoreThanLimit();
37+
38+
[Fact]
39+
public abstract void ThrowsWhenAcquiringLessThanZero();
40+
41+
[Fact]
42+
public abstract Task ThrowsWhenWaitingForLessThanZero();
43+
44+
[Fact]
45+
public abstract void AcquireZero_WithAvailability();
46+
47+
[Fact]
48+
public abstract void AcquireZero_WithoutAvailability();
49+
50+
[Fact]
51+
public abstract Task WaitAsyncZero_WithAvailability();
52+
53+
[Fact]
54+
public abstract Task WaitAsyncZero_WithoutAvailabilityWaitsForAvailability();
55+
56+
[Fact]
57+
public abstract Task CanDequeueMultipleResourcesAtOnce();
58+
59+
[Fact]
60+
public abstract Task CanAcquireResourcesWithWaitAsyncWithQueuedItemsIfNewestFirst();
61+
62+
[Fact]
63+
public abstract Task CannotAcquireResourcesWithWaitAsyncWithQueuedItemsIfOldestFirst();
64+
65+
[Fact]
66+
public abstract Task CanCancelWaitAsyncAfterQueuing();
67+
68+
[Fact]
69+
public abstract Task CanCancelWaitAsyncBeforeQueuing();
70+
71+
[Fact]
72+
public abstract Task CanAcquireResourcesWithAcquireWithQueuedItemsIfNewestFirst();
73+
74+
[Fact]
75+
public abstract Task CannotAcquireResourcesWithAcquireWithQueuedItemsIfOldestFirst();
76+
77+
[Fact]
78+
public abstract void NoMetadataOnAcquiredLease();
79+
}
80+
}

0 commit comments

Comments
 (0)