Created
June 15, 2019 23:45
-
-
Save ahancock1/a210c210b6175131d5064557416d4881 to your computer and use it in GitHub Desktop.
A weighted and timed rate limiter for c# that limits access to resources
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class Disposable : IDisposable | |
{ | |
private bool _disposed; | |
public void Dispose() | |
{ | |
if (_disposed) | |
{ | |
return; | |
} | |
Dispose(_disposed = true); | |
} | |
protected void ThrowIfDisposed(IDisposable disposable) | |
{ | |
if (_disposed) | |
{ | |
throw new ObjectDisposedException(disposable.GetType().Name); | |
} | |
} | |
protected abstract void Dispose(bool disposing); | |
} | |
public interface IRateLimiter : IDisposable | |
{ | |
TimeSpan Interval { get; set; } | |
int Limit { get; set; } | |
Task ThrottleAsync(int weight = 1, CancellationToken token = default); | |
} | |
public class RateLimiter : Disposable, IRateLimiter | |
{ | |
private readonly LinkedList<RateLimitRequest> _requests = new LinkedList<RateLimitRequest>(); | |
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | |
public RateLimiter(TimeSpan interval, int limit) | |
{ | |
Interval = interval; | |
Limit = limit; | |
} | |
public TimeSpan Interval { get; set; } | |
public int Limit { get; set; } | |
public async Task ThrottleAsync(int weight = 1, CancellationToken token = default) | |
{ | |
if (weight < 1) | |
{ | |
return; | |
} | |
ThrowIfDisposed(this); | |
var count = 0; | |
var current = DateTime.UtcNow; | |
var last = DateTime.MinValue; | |
var target = current - Interval; | |
await _semaphore.WaitAsync(token); | |
try | |
{ | |
var node = _requests.First; | |
while (node != null) | |
{ | |
var timestamp = node.Value.Timestamp; | |
var next = node.Next; | |
if (timestamp > target) | |
{ | |
if (count + weight <= Limit) | |
{ | |
last = timestamp; | |
count += node.Value.Weight; | |
} | |
} | |
else | |
{ | |
_requests.Remove(node); | |
} | |
node = next; | |
} | |
if (count + weight <= Limit) | |
{ | |
_requests.AddFirst(new RateLimitRequest | |
{ | |
Weight = weight, | |
Timestamp = DateTime.UtcNow | |
}); | |
return; | |
} | |
var delay = last.Add(Interval).Subtract(current); | |
await Task.Delay(delay, token); | |
_requests.AddFirst(new RateLimitRequest | |
{ | |
Weight = weight, | |
Timestamp = DateTime.UtcNow | |
}); | |
} | |
catch (Exception) | |
{ | |
/* Ignore */ | |
} | |
finally | |
{ | |
_semaphore.Release(); | |
} | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
if (!disposing) | |
{ | |
return; | |
} | |
_semaphore?.Dispose(); | |
} | |
private class RateLimitRequest | |
{ | |
public DateTime Timestamp { get; set; } | |
public int Weight { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment