4
4
// Pending dotnet API review
5
5
6
6
using System . Collections . Generic ;
7
+ using System . Diagnostics ;
8
+ using System . Diagnostics . CodeAnalysis ;
7
9
using System . Threading . Tasks ;
8
10
9
11
namespace System . Threading . RateLimiting
10
12
{
11
- #pragma warning disable 1591
13
+ /// <summary>
14
+ /// <see cref="RateLimiter"/> implementation that replenishes tokens periodically instead of via a release mechanism.
15
+ /// </summary>
12
16
public sealed class TokenBucketRateLimiter : RateLimiter
13
17
{
14
18
private int _tokenCount ;
15
19
private int _queueCount ;
16
- private long _lastReplenishmentTick ;
20
+ private uint _lastReplenishmentTick = ( uint ) Environment . TickCount ;
17
21
18
22
private readonly Timer ? _renewTimer ;
19
- private readonly object _lock = new object ( ) ;
20
23
private readonly TokenBucketRateLimiterOptions _options ;
21
24
private readonly Deque < RequestRegistration > _queue = new Deque < RequestRegistration > ( ) ;
22
25
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
+
23
29
private static readonly RateLimitLease SuccessfulLease = new TokenBucketLease ( true , null ) ;
24
30
31
+ /// <summary>
32
+ /// Initializes the <see cref="TokenBucketRateLimiter"/>.
33
+ /// </summary>
34
+ /// <param name="options">Options to specify the behavior of the <see cref="TokenBucketRateLimiter"/>.</param>
25
35
public TokenBucketRateLimiter ( TokenBucketRateLimiterOptions options )
26
36
{
27
37
_tokenCount = options . TokenLimit ;
@@ -33,84 +43,135 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
33
43
}
34
44
}
35
45
46
+ /// <inheritdoc/>
36
47
public override int GetAvailablePermits ( ) => _tokenCount ;
37
48
49
+ /// <inheritdoc/>
38
50
protected override RateLimitLease AcquireCore ( int tokenCount )
39
51
{
40
52
// These amounts of resources can never be acquired
41
53
if ( tokenCount > _options . TokenLimit )
42
54
{
43
- 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 } .") ;
44
56
}
45
57
46
- // Return SuccessfulAcquisition or FailedAcquisition depending to indicate limiter state
58
+ // Return SuccessfulLease or FailedLease depending to indicate limiter state
47
59
if ( tokenCount == 0 )
48
60
{
49
- if ( GetAvailablePermits ( ) > 0 )
61
+ if ( _tokenCount > 0 )
50
62
{
51
63
return SuccessfulLease ;
52
64
}
53
65
54
- return CreateFailedTokenLease ( ) ;
66
+ return CreateFailedTokenLease ( tokenCount ) ;
55
67
}
56
68
57
- // These amounts of resources can never be acquired
58
- if ( Interlocked . Add ( ref _tokenCount , - tokenCount ) >= 0 )
69
+ lock ( Lock )
59
70
{
60
- return SuccessfulLease ;
61
- }
62
-
63
- Interlocked . Add ( ref _tokenCount , tokenCount ) ;
71
+ if ( TryLeaseUnsynchronized ( tokenCount , out RateLimitLease ? lease ) )
72
+ {
73
+ return lease ;
74
+ }
64
75
65
- return CreateFailedTokenLease ( ) ;
76
+ return CreateFailedTokenLease ( tokenCount ) ;
77
+ }
66
78
}
67
79
80
+ /// <inheritdoc/>
68
81
protected override ValueTask < RateLimitLease > WaitAsyncCore ( int tokenCount , CancellationToken cancellationToken = default )
69
82
{
83
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
84
+
70
85
// These amounts of resources can never be acquired
71
- if ( tokenCount < 0 || tokenCount > _options . TokenLimit )
86
+ if ( tokenCount > _options . TokenLimit )
72
87
{
73
- throw new ArgumentOutOfRangeException ( ) ;
88
+ throw new ArgumentOutOfRangeException ( nameof ( tokenCount ) , $ " { tokenCount } token(s) exceeds the permit limit of { _options . TokenLimit } ." ) ;
74
89
}
75
90
76
91
// Return SuccessfulAcquisition if requestedCount is 0 and resources are available
77
- if ( tokenCount == 0 && GetAvailablePermits ( ) > 0 )
92
+ if ( tokenCount == 0 && _tokenCount > 0 )
78
93
{
79
- // Perf: static failed/successful value tasks?
80
94
return new ValueTask < RateLimitLease > ( SuccessfulLease ) ;
81
95
}
82
96
83
- if ( Interlocked . Add ( ref _tokenCount , - tokenCount ) >= 0 )
97
+ lock ( Lock )
84
98
{
85
- // Perf: static failed/successful value tasks?
86
- return new ValueTask < RateLimitLease > ( SuccessfulLease ) ;
87
- }
99
+ if ( TryLeaseUnsynchronized ( tokenCount , out RateLimitLease ? lease ) )
100
+ {
101
+ return new ValueTask < RateLimitLease > ( lease ) ;
102
+ }
103
+
104
+ // Don't queue if queue limit reached
105
+ if ( _queueCount + tokenCount > _options . QueueLimit )
106
+ {
107
+ return new ValueTask < RateLimitLease > ( CreateFailedTokenLease ( tokenCount ) ) ;
108
+ }
88
109
89
- Interlocked . Add ( ref _tokenCount , tokenCount ) ;
110
+ TaskCompletionSource < RateLimitLease > tcs = new TaskCompletionSource < RateLimitLease > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
90
111
91
- // Don't queue if queue limit reached
92
- if ( _queueCount + tokenCount > _options . QueueLimit )
93
- {
94
- return new ValueTask < RateLimitLease > ( CreateFailedTokenLease ( ) ) ;
112
+ CancellationTokenRegistration ctr ;
113
+ if ( cancellationToken . CanBeCanceled )
114
+ {
115
+ ctr = cancellationToken . Register ( obj =>
116
+ {
117
+ ( ( TaskCompletionSource < RateLimitLease > ) obj ) . TrySetException ( new OperationCanceledException ( cancellationToken ) ) ;
118
+ } , tcs ) ;
119
+ }
120
+
121
+ RequestRegistration registration = new RequestRegistration ( tokenCount , tcs , ctr ) ;
122
+ _queue . EnqueueTail ( registration ) ;
123
+ _queueCount += tokenCount ;
124
+ Debug . Assert ( _queueCount <= _options . QueueLimit ) ;
125
+
126
+ // handle cancellation
127
+ return new ValueTask < RateLimitLease > ( registration . Tcs . Task ) ;
95
128
}
129
+ }
96
130
97
- var registration = new RequestRegistration ( tokenCount ) ;
98
- _queue . EnqueueTail ( registration ) ;
99
- Interlocked . Add ( ref _tokenCount , tokenCount ) ;
131
+ private RateLimitLease CreateFailedTokenLease ( int tokenCount )
132
+ {
133
+ int replenishAmount = tokenCount - _tokenCount + _queueCount ;
134
+ // can't have 0 replenish periods, that would mean it should be a successful lease
135
+ // if TokensPerPeriod is larger than the replenishAmount needed then it would be 0
136
+ int replenishPeriods = Math . Max ( replenishAmount / _options . TokensPerPeriod , 1 ) ;
100
137
101
- // handle cancellation
102
- return new ValueTask < RateLimitLease > ( registration . Tcs . Task ) ;
138
+ return new TokenBucketLease ( false , TimeSpan . FromTicks ( _options . ReplenishmentPeriod . Ticks * replenishPeriods ) ) ;
103
139
}
104
140
105
- private RateLimitLease CreateFailedTokenLease ( )
141
+ private bool TryLeaseUnsynchronized ( int tokenCount , [ NotNullWhen ( true ) ] out RateLimitLease ? lease )
106
142
{
107
- var replenishAmount = _tokenCount - GetAvailablePermits ( ) + _queueCount ;
108
- var replenishPeriods = ( replenishAmount / _options . TokensPerPeriod ) + 1 ;
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
+ }
109
152
110
- return new TokenBucketLease ( false , TimeSpan . FromTicks ( _options . ReplenishmentPeriod . Ticks * replenishPeriods ) ) ;
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 ;
111
166
}
112
167
113
- // Attempts to replenish the bucket, returns triue if enough time has elapsed and it replenishes; otherwise, false.
168
+ /// <summary>
169
+ /// Attempts to replenish the bucket.
170
+ /// </summary>
171
+ /// <returns>
172
+ /// False if <see cref="TokenBucketRateLimiterOptions.AutoReplenishment"/> is enabled, otherwise true.
173
+ /// Does not reflect if tokens were replenished.
174
+ /// </returns>
114
175
public bool TryReplenish ( )
115
176
{
116
177
if ( _options . AutoReplenishment )
@@ -123,60 +184,86 @@ public bool TryReplenish()
123
184
124
185
private static void Replenish ( object ? state )
125
186
{
126
- // Return if Replenish already running to avoid concurrency.
127
- if ( ! ( state is TokenBucketRateLimiter ) )
128
- {
129
- return ;
130
- }
187
+ TokenBucketRateLimiter limiter = ( state as TokenBucketRateLimiter ) ! ;
188
+ Debug . Assert ( limiter is not null ) ;
131
189
132
- var limiter = ( TokenBucketRateLimiter ) state ;
190
+ // Use Environment.TickCount instead of DateTime.UtcNow to avoid issues on systems where the clock can change
191
+ uint nowTicks = ( uint ) Environment . TickCount ;
192
+ limiter ! . ReplenishInternal ( nowTicks ) ;
193
+ }
133
194
134
- var nowTicks = DateTime . Now . Ticks ;
135
- // Need to acount for multiple periods. Need to account for ticks right below the replenishment period.
136
- if ( nowTicks - limiter . _lastReplenishmentTick < limiter . _options . ReplenishmentPeriod . Ticks )
195
+ // Used in tests that test behavior with specific time intervals
196
+ internal void ReplenishInternal ( uint nowTicks )
197
+ {
198
+ bool wrapped = false ;
199
+ // (uint)TickCount will wrap every ~50 days, we can detect that by checking if the new ticks is less than the last replenishment
200
+ if ( nowTicks < _lastReplenishmentTick )
137
201
{
138
- return ;
202
+ wrapped = true ;
139
203
}
140
204
141
- limiter . _lastReplenishmentTick = nowTicks ;
205
+ // method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes
206
+ lock ( Lock )
207
+ {
208
+ // Fix the wrapping by using a long and adding uint.MaxValue in the wrapped case
209
+ long nonWrappedTicks = wrapped ? ( long ) nowTicks + uint . MaxValue : nowTicks ;
210
+ if ( nonWrappedTicks - _lastReplenishmentTick < _options . ReplenishmentPeriod . TotalMilliseconds )
211
+ {
212
+ return ;
213
+ }
142
214
143
- var availablePermits = limiter . GetAvailablePermits ( ) ;
144
- var options = limiter . _options ;
145
- var maxPermits = options . TokenLimit ;
215
+ _lastReplenishmentTick = nowTicks ;
146
216
147
- if ( availablePermits < maxPermits )
148
- {
149
- var resoucesToAdd = Math . Min ( options . TokensPerPeriod , maxPermits - availablePermits ) ;
150
- Interlocked . Add ( ref limiter . _tokenCount , resoucesToAdd ) ;
151
- }
217
+ int availablePermits = _tokenCount ;
218
+ TokenBucketRateLimiterOptions options = _options ;
219
+ int maxPermits = options . TokenLimit ;
220
+ int resourcesToAdd ;
152
221
153
- // Process queued requests
154
- var queue = limiter . _queue ;
155
- lock ( limiter . _lock )
156
- {
222
+ if ( availablePermits < maxPermits )
223
+ {
224
+ resourcesToAdd = Math . Min ( options . TokensPerPeriod , maxPermits - availablePermits ) ;
225
+ }
226
+ else
227
+ {
228
+ // All tokens available, nothing to do
229
+ return ;
230
+ }
231
+
232
+ // Process queued requests
233
+ Deque < RequestRegistration > queue = _queue ;
234
+
235
+ _tokenCount += resourcesToAdd ;
236
+ Debug . Assert ( _tokenCount <= _options . TokenLimit ) ;
157
237
while ( queue . Count > 0 )
158
238
{
159
- var nextPendingRequest =
239
+ RequestRegistration nextPendingRequest =
160
240
options . QueueProcessingOrder == QueueProcessingOrder . OldestFirst
161
241
? queue . PeekHead ( )
162
242
: queue . PeekTail ( ) ;
163
243
164
- if ( Interlocked . Add ( ref limiter . _tokenCount , - nextPendingRequest . Count ) >= 0 )
244
+ if ( _tokenCount >= nextPendingRequest . Count )
165
245
{
166
246
// Request can be fulfilled
167
- var request =
247
+ nextPendingRequest =
168
248
options . QueueProcessingOrder == QueueProcessingOrder . OldestFirst
169
249
? queue . DequeueHead ( )
170
250
: queue . DequeueTail ( ) ;
171
- Interlocked . Add ( ref limiter . _queueCount , - request . Count ) ;
172
251
173
- // requestToFulfill == request
174
- request . Tcs . SetResult ( SuccessfulLease ) ;
252
+ _queueCount -= nextPendingRequest . Count ;
253
+ _tokenCount -= nextPendingRequest . Count ;
254
+ Debug . Assert ( _queueCount >= 0 ) ;
255
+ Debug . Assert ( _tokenCount >= 0 ) ;
256
+
257
+ if ( ! nextPendingRequest . Tcs . TrySetResult ( SuccessfulLease ) )
258
+ {
259
+ // Queued item was canceled so add count back
260
+ _tokenCount += nextPendingRequest . Count ;
261
+ }
262
+ nextPendingRequest . CancellationTokenRegistration . Dispose ( ) ;
175
263
}
176
264
else
177
265
{
178
266
// Request cannot be fulfilled
179
- Interlocked . Add ( ref limiter . _tokenCount , nextPendingRequest . Count ) ;
180
267
break ;
181
268
}
182
269
}
@@ -195,7 +282,15 @@ public TokenBucketLease(bool isAcquired, TimeSpan? retryAfter)
195
282
196
283
public override bool IsAcquired { get ; }
197
284
198
- public override IEnumerable < string > MetadataNames => throw new NotImplementedException ( ) ;
285
+ public override IEnumerable < string > MetadataNames => Enumerable ( ) ;
286
+
287
+ private IEnumerable < string > Enumerable ( )
288
+ {
289
+ if ( _retryAfter is not null )
290
+ {
291
+ yield return MetadataName . RetryAfter . Name ;
292
+ }
293
+ }
199
294
200
295
public override bool TryGetMetadata ( string metadataName , out object ? metadata )
201
296
{
@@ -205,26 +300,29 @@ public override bool TryGetMetadata(string metadataName, out object? metadata)
205
300
return true ;
206
301
}
207
302
208
- metadata = null ;
303
+ metadata = default ;
209
304
return false ;
210
305
}
211
306
212
307
protected override void Dispose ( bool disposing ) { }
213
308
}
214
309
215
- private struct RequestRegistration
310
+ private readonly struct RequestRegistration
216
311
{
217
- public RequestRegistration ( int tokenCount )
312
+ public RequestRegistration ( int tokenCount , TaskCompletionSource < RateLimitLease > tcs , CancellationTokenRegistration cancellationTokenRegistration )
218
313
{
219
314
Count = tokenCount ;
220
315
// Use VoidAsyncOperationWithData<T> instead
221
- Tcs = new TaskCompletionSource < RateLimitLease > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
316
+ Tcs = tcs ;
317
+ CancellationTokenRegistration = cancellationTokenRegistration ;
222
318
}
223
319
224
320
public int Count { get ; }
225
321
226
322
public TaskCompletionSource < RateLimitLease > Tcs { get ; }
323
+
324
+ public CancellationTokenRegistration CancellationTokenRegistration { get ; }
325
+
227
326
}
228
327
}
229
- #pragma warning restore
230
328
}
0 commit comments