Skip to content

Instantly share code, notes, and snippets.

@ahancock1
Created June 15, 2019 23:45
Show Gist options
  • Save ahancock1/a210c210b6175131d5064557416d4881 to your computer and use it in GitHub Desktop.
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
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