using System;
using System.Collections.Generic;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class FixedWindowTracker : IWindowTracker
{
///
public TimeSpan TimePeriod { get; }
///
public int Limit { get; }
///
public int Current => _currentWeight;
private readonly Queue _entries;
private int _currentWeight = 0;
///
/// Additional wait time to apply to account for time offset between server and client
///
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
TimePeriod = period;
_entries = new Queue();
}
///
public void Reset(int? amount)
{
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries.Dequeue();
removedWeight += lastEntry.Weight;
}
}
}
///
public TimeSpan GetWaitTime(int weight)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)));
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 can fit without going over limit
return TimeSpan.Zero;
}
///
public void ApplyWeight(int weight)
{
_currentWeight += weight;
_entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight));
}
///
/// Remove items before a certain time
///
///
protected void RemoveBefore(DateTime time)
{
while (true)
{
if (_entries.Count == 0)
break;
var firstItem = _entries.Peek();
if (firstItem.Timestamp < time)
{
_entries.Dequeue();
_currentWeight -= firstItem.Weight;
}
else
{
// Either no entries left, or the entry time is still within the window
break;
}
}
}
///
/// Determine the time to wait before a new item would fit
///
///
private TimeSpan DetermineWaitTime()
{
var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
var result = wait.Add(_fixedWindowBuffer);
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
}