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; } } }