1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2026-04-12 16:13:12 +00:00

116 lines
3.7 KiB
C#

using System;
using System.Collections.Generic;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class SlidingWindowTracker : IWindowTracker
{
/// <inheritdoc />
public TimeSpan TimePeriod { get; }
/// <inheritdoc />
public int Limit { get; }
/// <inheritdoc />
public int Current => _currentWeight;
private readonly List<LimitEntry> _entries;
private int _currentWeight = 0;
/// <summary>
/// Additional wait time to apply to account for fluctuating request times
/// </summary>
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
public SlidingWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
TimePeriod = period;
_entries = new List<LimitEntry>();
}
/// <inheritdoc />
public void Reset()
{
_entries.Clear();
_currentWeight = 0;
}
/// <inheritdoc />
public TimeSpan GetWaitTime(int weight)
{
// Remove requests no longer in time period from the history
RemoveBefore(DateTime.UtcNow - TimePeriod);
if (Current + weight > Limit)
{
// The weight would cause the rate limit to be passed
if (Current == 0)
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
}
// Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime(weight);
}
// Weight can fit without going over limit
return TimeSpan.Zero;
}
/// <inheritdoc />
public void ApplyWeight(int weight)
{
_currentWeight += weight;
_entries.Add(new LimitEntry(DateTime.UtcNow, weight));
}
/// <summary>
/// Remove items before a certain time
/// </summary>
/// <param name="time"></param>
protected void RemoveBefore(DateTime time)
{
for (var i = 0; i < _entries.Count; i++)
{
if (_entries[i].Timestamp < time)
{
var entry = _entries[i];
_entries.Remove(entry);
_currentWeight -= entry.Weight;
i--;
}
else
{
break;
}
}
}
/// <summary>
/// Determine the time to wait before the weight would fit
/// </summary>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight)
{
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
var removedWeight = 0;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
removedWeight += entry.Weight;
if (removedWeight >= weightToRemove)
{
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
throw new Exception("Request not possible to execute with current rate limit guard. " +
$" Request weight: {requestWeight}, RateLimit: {Limit}");
}
}
}