Skip to content

Commit 8b5185a

Browse files
github-actions[bot]BrennanConroyhalter73
authored
[release/7.0] [RateLimiting] Handle Timer jitter (#74971)
* [RateLimiting] TryReplenish handles multiple replenish periods at a time * ignore tick on auto * fixup * partial * allow TimeSpan.Zero * no special case * TimeSpan.Zero * Apply suggestions from code review Co-authored-by: Stephen Halter <[email protected]> Co-authored-by: Brennan Conroy <[email protected]> Co-authored-by: Stephen Halter <[email protected]>
1 parent d2e3961 commit 8b5185a

File tree

9 files changed

+562
-359
lines changed

9 files changed

+562
-359
lines changed

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiter.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ public FixedWindowRateLimiter(FixedWindowRateLimiterOptions options)
5959
{
6060
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
6161
}
62-
if (options.Window < TimeSpan.Zero)
62+
if (options.Window <= TimeSpan.Zero)
6363
{
64-
throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
64+
throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
6565
}
6666

6767
_options = new FixedWindowRateLimiterOptions
@@ -287,29 +287,22 @@ private void ReplenishInternal(long nowTicks)
287287
return;
288288
}
289289

290-
if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks)
290+
if (((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.Window.Ticks && !_options.AutoReplenishment)
291291
{
292292
return;
293293
}
294294

295295
_lastReplenishmentTick = nowTicks;
296296

297297
int availableRequestCounters = _requestCount;
298-
int maxPermits = _options.PermitLimit;
299-
int resourcesToAdd;
300298

301-
if (availableRequestCounters < maxPermits)
302-
{
303-
resourcesToAdd = maxPermits - availableRequestCounters;
304-
}
305-
else
299+
if (availableRequestCounters >= _options.PermitLimit)
306300
{
307301
// All counters available, nothing to do
308302
return;
309303
}
310304

311-
_requestCount += resourcesToAdd;
312-
Debug.Assert(_requestCount == _options.PermitLimit);
305+
_requestCount = _options.PermitLimit;
313306

314307
// Process queued requests
315308
while (_queue.Count > 0)

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/FixedWindowRateLimiterOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class FixedWindowRateLimiterOptions
1010
{
1111
/// <summary>
1212
/// Specifies the time window that takes in the requests.
13-
/// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="FixedWindowRateLimiter"/>.
13+
/// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="FixedWindowRateLimiter"/>.
1414
/// </summary>
1515
public TimeSpan Window { get; set; } = TimeSpan.Zero;
1616

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiter.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter
2626

2727
private readonly Timer? _renewTimer;
2828
private readonly SlidingWindowRateLimiterOptions _options;
29+
private readonly TimeSpan _replenishmentPeriod;
2930
private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>();
3031

3132
// 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
@@ -42,7 +43,7 @@ public sealed class SlidingWindowRateLimiter : ReplenishingRateLimiter
4243
public override bool IsAutoReplenishing => _options.AutoReplenishment;
4344

4445
/// <inheritdoc />
45-
public override TimeSpan ReplenishmentPeriod => new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow);
46+
public override TimeSpan ReplenishmentPeriod => _replenishmentPeriod;
4647

4748
/// <summary>
4849
/// Initializes the <see cref="SlidingWindowRateLimiter"/>.
@@ -62,9 +63,9 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options)
6263
{
6364
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
6465
}
65-
if (options.Window < TimeSpan.Zero)
66+
if (options.Window <= TimeSpan.Zero)
6667
{
67-
throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
68+
throw new ArgumentException($"{nameof(options.Window)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
6869
}
6970

7071
_options = new SlidingWindowRateLimiterOptions
@@ -78,6 +79,7 @@ public SlidingWindowRateLimiter(SlidingWindowRateLimiterOptions options)
7879
};
7980

8081
_requestCount = options.PermitLimit;
82+
_replenishmentPeriod = new TimeSpan(_options.Window.Ticks / _options.SegmentsPerWindow);
8183

8284
// _requestsPerSegment holds the no. of acquired requests in each window segment
8385
_requestsPerSegment = new int[options.SegmentsPerWindow];
@@ -287,7 +289,7 @@ private void ReplenishInternal(long nowTicks)
287289
return;
288290
}
289291

290-
if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < ReplenishmentPeriod.Ticks)
292+
if (((nowTicks - _lastReplenishmentTick) * TickFrequency) < ReplenishmentPeriod.Ticks && !_options.AutoReplenishment)
291293
{
292294
return;
293295
}

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/SlidingWindowRateLimiterOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class SlidingWindowRateLimiterOptions
1010
{
1111
/// <summary>
1212
/// Specifies the minimum period between replenishments.
13-
/// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="SlidingWindowRateLimiter"/>.
13+
/// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="SlidingWindowRateLimiter"/>.
1414
/// </summary>
1515
public TimeSpan Window { get; set; } = TimeSpan.Zero;
1616

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiter.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace System.Threading.RateLimiting
1313
/// </summary>
1414
public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter
1515
{
16-
private int _tokenCount;
16+
private double _tokenCount;
1717
private int _queueCount;
1818
private long _lastReplenishmentTick;
1919
private long? _idleSince;
@@ -22,6 +22,7 @@ public sealed class TokenBucketRateLimiter : ReplenishingRateLimiter
2222
private long _failedLeasesCount;
2323
private long _successfulLeasesCount;
2424

25+
private readonly double _fillRate;
2526
private readonly Timer? _renewTimer;
2627
private readonly TokenBucketRateLimiterOptions _options;
2728
private readonly Deque<RequestRegistration> _queue = new Deque<RequestRegistration>();
@@ -60,9 +61,9 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
6061
{
6162
throw new ArgumentException($"{nameof(options.QueueLimit)} must be set to a value greater than or equal to 0.", nameof(options));
6263
}
63-
if (options.ReplenishmentPeriod < TimeSpan.Zero)
64+
if (options.ReplenishmentPeriod <= TimeSpan.Zero)
6465
{
65-
throw new ArgumentException($"{nameof(options.ReplenishmentPeriod)} must be set to a value greater than or equal to TimeSpan.Zero.", nameof(options));
66+
throw new ArgumentException($"{nameof(options.ReplenishmentPeriod)} must be set to a value greater than TimeSpan.Zero.", nameof(options));
6667
}
6768

6869
_options = new TokenBucketRateLimiterOptions
@@ -76,6 +77,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
7677
};
7778

7879
_tokenCount = options.TokenLimit;
80+
_fillRate = (double)options.TokensPerPeriod / options.ReplenishmentPeriod.Ticks;
7981

8082
_idleSince = _lastReplenishmentTick = Stopwatch.GetTimestamp();
8183

@@ -91,7 +93,7 @@ public TokenBucketRateLimiter(TokenBucketRateLimiterOptions options)
9193
ThrowIfDisposed();
9294
return new RateLimiterStatistics()
9395
{
94-
CurrentAvailablePermits = _tokenCount,
96+
CurrentAvailablePermits = (long)_tokenCount,
9597
CurrentQueuedCount = _queueCount,
9698
TotalFailedLeases = Interlocked.Read(ref _failedLeasesCount),
9799
TotalSuccessfulLeases = Interlocked.Read(ref _successfulLeasesCount),
@@ -210,7 +212,7 @@ protected override ValueTask<RateLimitLease> AcquireAsyncCore(int tokenCount, Ca
210212

211213
private RateLimitLease CreateFailedTokenLease(int tokenCount)
212214
{
213-
int replenishAmount = tokenCount - _tokenCount + _queueCount;
215+
int replenishAmount = tokenCount - (int)_tokenCount + _queueCount;
214216
// can't have 0 replenish periods, that would mean it should be a successful lease
215217
// if TokensPerPeriod is larger than the replenishAmount needed then it would be 0
216218
Debug.Assert(_options.TokensPerPeriod > 0);
@@ -278,7 +280,7 @@ private static void Replenish(object? state)
278280
limiter!.ReplenishInternal(nowTicks);
279281
}
280282

281-
// Used in tests that test behavior with specific time intervals
283+
// Used in tests to avoid dealing with real time
282284
private void ReplenishInternal(long nowTicks)
283285
{
284286
// method is re-entrant (from Timer), lock to avoid multiple simultaneous replenishes
@@ -289,45 +291,43 @@ private void ReplenishInternal(long nowTicks)
289291
return;
290292
}
291293

292-
if ((long)((nowTicks - _lastReplenishmentTick) * TickFrequency) < _options.ReplenishmentPeriod.Ticks)
294+
if (_tokenCount == _options.TokenLimit)
293295
{
294296
return;
295297
}
296298

297-
_lastReplenishmentTick = nowTicks;
298-
299-
int availablePermits = _tokenCount;
300-
TokenBucketRateLimiterOptions options = _options;
301-
int maxPermits = options.TokenLimit;
302-
int resourcesToAdd;
299+
double add;
303300

304-
if (availablePermits < maxPermits)
301+
// Trust the timer to be close enough to when we want to replenish, this avoids issues with Timer jitter where it might be .99 seconds instead of 1, and 1.1 seconds the next time etc.
302+
if (_options.AutoReplenishment)
305303
{
306-
resourcesToAdd = Math.Min(options.TokensPerPeriod, maxPermits - availablePermits);
304+
add = _options.TokensPerPeriod;
307305
}
308306
else
309307
{
310-
// All tokens available, nothing to do
311-
return;
308+
add = _fillRate * (nowTicks - _lastReplenishmentTick) * TickFrequency;
312309
}
313310

311+
_tokenCount = Math.Min(_options.TokenLimit, _tokenCount + add);
312+
313+
_lastReplenishmentTick = nowTicks;
314+
314315
// Process queued requests
315316
Deque<RequestRegistration> queue = _queue;
316317

317-
_tokenCount += resourcesToAdd;
318318
Debug.Assert(_tokenCount <= _options.TokenLimit);
319319
while (queue.Count > 0)
320320
{
321321
RequestRegistration nextPendingRequest =
322-
options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
322+
_options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
323323
? queue.PeekHead()
324324
: queue.PeekTail();
325325

326326
if (_tokenCount >= nextPendingRequest.Count)
327327
{
328328
// Request can be fulfilled
329329
nextPendingRequest =
330-
options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
330+
_options.QueueProcessingOrder == QueueProcessingOrder.OldestFirst
331331
? queue.DequeueHead()
332332
: queue.DequeueTail();
333333

src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/TokenBucketRateLimiterOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class TokenBucketRateLimiterOptions
1010
{
1111
/// <summary>
1212
/// Specifies the minimum period between replenishments.
13-
/// Must be set to a value >= <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="TokenBucketRateLimiter"/>.
13+
/// Must be set to a value greater than <see cref="TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="TokenBucketRateLimiter"/>.
1414
/// </summary>
1515
public TimeSpan ReplenishmentPeriod { get; set; } = TimeSpan.Zero;
1616

0 commit comments

Comments
 (0)