1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-15 15:40:52 +00:00

Fixed issues in DecayWindowTracker calculations

This commit is contained in:
Jkorf
2026-09-21 13:43:05 +02:00
parent b9f93cb3fe
commit b8baaf55b5
@@ -18,9 +18,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private int _currentWeight = 0; private int _currentWeight = 0;
private DateTime _lastDecrease = DateTime.UtcNow; private DateTime _lastDecrease = DateTime.UtcNow;
private double _decayProgress;
public DecayWindowTracker(int limit, TimeSpan period, double decayRate) public DecayWindowTracker(
int limit,
TimeSpan period,
double decayRate)
{ {
if (period <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(period));
if (decayRate <= 0)
throw new ArgumentOutOfRangeException(nameof(decayRate));
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
DecreaseRate = decayRate; DecreaseRate = decayRate;
@@ -31,83 +40,118 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
if (amount == null) if (amount == null)
{ {
_lastDecrease = DateTime.UtcNow; ResetState();
_currentWeight = 0; return;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
} }
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
if (_currentWeight == 0)
ResetState();
} }
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight, double allowedRateRatio) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Decrease the counter based on the last update time and decay rate var now = DateTime.UtcNow;
DecreaseCounter(DateTime.UtcNow); DecreaseCounter(now);
if ((Current + weight) / (double)Limit > allowedRateRatio) if ((Current + weight) / (double)Limit <= allowedRateRatio)
return TimeSpan.Zero;
if (Current == 0)
{ {
// The weight would cause the rate limit to be passed if (allowedRateRatio < 1)
if (Current == 0)
{ {
if (allowedRateRatio < 1) throw new Exception(
{ "Request limit reached max utilization. " +
throw new Exception("Request limit reached max utilization. " + "This request can never execute with the current rate limiter configuration. " +
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " + $"Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, AllowedRateRatio: {allowedRateRatio}"); $"Request ratio: {(Current + weight) / (double)Limit}, " +
} $"AllowedRateRatio: {allowedRateRatio}");
else
{
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 throw new Exception(
return DetermineWaitTime(weight, allowedRateRatio); "Request limit reached without any prior request. " +
"This request can never execute with the current rate limiter. " +
$"Request weight: {weight}, RateLimit: {Limit}");
} }
// Weight can fit without going over limit return DetermineWaitTime(
return TimeSpan.Zero; weight,
allowedRateRatio);
} }
/// <inheritdoc />
public void ApplyWeight(int weight) public void ApplyWeight(int weight)
{ {
if (_currentWeight == 0) if (_currentWeight == 0)
{
_lastDecrease = DateTime.UtcNow; _lastDecrease = DateTime.UtcNow;
_decayProgress = 0;
}
_currentWeight += weight; _currentWeight += weight;
} }
/// <summary> private void DecreaseCounter(DateTime now)
/// Decrease the counter based on time passed since last update and the decay rate
/// </summary>
/// <param name="time"></param>
protected void DecreaseCounter(DateTime time)
{ {
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate; if (_currentWeight == 0)
var decrease = (int)Math.Floor(dif);
if (decrease >= 1)
{ {
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif)); _lastDecrease = now;
_lastDecrease = time; _decayProgress = 0;
return;
}
var elapsed = now - _lastDecrease;
if (elapsed <= TimeSpan.Zero)
return;
var elapsedDecay = elapsed.Ticks / (double)TimePeriod.Ticks * DecreaseRate;
var totalDecay = _decayProgress + elapsedDecay;
var completedDecay = (int)Math.Floor(totalDecay);
_lastDecrease = now;
if (completedDecay == 0)
{
_decayProgress = totalDecay;
return;
}
_currentWeight = Math.Max(0, _currentWeight - completedDecay);
if (_currentWeight == 0)
{
// Decay cannot accumulate as credit while the counter is empty.
_decayProgress = 0;
}
else
{
_decayProgress = totalDecay - completedDecay;
} }
} }
/// <summary> private TimeSpan DetermineWaitTime(
/// Determine the time to wait before the weight would fit int requestWeight,
/// </summary> double allowedRateRatio)
/// <param name="requestWeight"></param>
/// <param name="allowedRateRatio"></param>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight, double allowedRateRatio)
{ {
var weightToRemove = Math.Max(Current + requestWeight - Limit * allowedRateRatio, 0); var weightToRemove = Current + requestWeight - Limit * allowedRateRatio;
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
if (result < TimeSpan.Zero) // The counter is integer-valued, so enough whole weight units
return TimeSpan.Zero; // must decay before the request can be admitted.
return result; var requiredDecay = Math.Ceiling(weightToRemove);
var remainingDecay = Math.Max(0, requiredDecay - _decayProgress);
var waitTicks = Math.Ceiling(remainingDecay / DecreaseRate * TimePeriod.Ticks);
return waitTicks <= 0
? TimeSpan.Zero
: TimeSpan.FromTicks((long)waitTicks);
}
private void ResetState()
{
_currentWeight = 0;
_decayProgress = 0;
_lastDecrease = DateTime.UtcNow;
} }
} }
} }