mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-10-13 22:51:39 +00:00
Compare commits
3 Commits
b9f93cb3fe
...
927ffe198b
| Author | SHA1 | Date | |
|---|---|---|---|
| 927ffe198b | |||
| ab17a88e33 | |||
| b8baaf55b5 |
@@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Filters;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using CryptoExchange.Net.UnitTests.Implementations;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
@@ -364,5 +365,30 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitingBehaviour.Fail, null, 1.0, default);
|
||||
Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>());
|
||||
}
|
||||
|
||||
[TestCase(0.1, 10)]
|
||||
[TestCase(1, 50)]
|
||||
[TestCase(5, 250)]
|
||||
[TestCase(60, 250)]
|
||||
public void RateLimitSafetyMargin_DefaultIsProportionalAndCapped(double periodSeconds, int expectedMarginMilliseconds)
|
||||
{
|
||||
var margin = WindowTrackerHelpers.GetDefaultSafetyMargin(TimeSpan.FromSeconds(periodSeconds));
|
||||
|
||||
Assert.That(margin, Is.EqualTo(TimeSpan.FromMilliseconds(expectedMarginMilliseconds)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RateLimitGuard_ExplicitSafetyMarginIsUsed()
|
||||
{
|
||||
var guard = new RateLimitGuard(
|
||||
RateLimitGuard.PerHost,
|
||||
new LimitItemTypeFilter(RateLimitItemType.Request),
|
||||
1,
|
||||
TimeSpan.FromSeconds(1),
|
||||
RateLimitWindowType.Sliding,
|
||||
safetyMargin: TimeSpan.Zero);
|
||||
|
||||
Assert.That(guard.SafetyMargin, Is.EqualTo(TimeSpan.Zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,18 @@ public interface ICallResult<T> : ICallResult
|
||||
/// <summary>
|
||||
/// Exchange call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface IExchangeCallResult<T> : ICallResult, ICallResult<T>
|
||||
public interface IExchangeCallResult : ICallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; set; }
|
||||
public string Exchange { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exchange call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface IExchangeCallResult<T> : IExchangeCallResult, ICallResult<T>
|
||||
{
|
||||
}
|
||||
@@ -36,6 +36,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
private readonly int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
||||
@@ -54,6 +55,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// The time period for the limit
|
||||
/// </summary>
|
||||
public TimeSpan TimeSpan { get; }
|
||||
/// <summary>
|
||||
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
|
||||
/// </summary>
|
||||
public TimeSpan SafetyMargin => _safetyMargin;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this guard is shared between multiple gates
|
||||
@@ -68,11 +73,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="limit">Limit per period</param>
|
||||
/// <param name="timeSpan">Timespan for the period</param>
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, TimeSpan? safetyMargin = null, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, safetyMargin, decayPerTimeSpan, connectionWeight, shared)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -84,10 +90,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="limit">Limit per period</param>
|
||||
/// <param name="timeSpan">Timespan for the period</param>
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, TimeSpan? safetyMargin = null, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
@@ -97,6 +104,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
SharedGuard = shared;
|
||||
_keySelector = keySelector;
|
||||
_decayRate = decayPerTimeSpan;
|
||||
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(timeSpan); ;
|
||||
_connectionWeight = connectionWeight;
|
||||
|
||||
if (SharedGuard)
|
||||
@@ -205,9 +213,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker()
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan)
|
||||
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) :
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan, _safetyMargin) :
|
||||
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
private readonly int _limit;
|
||||
private readonly TimeSpan _period;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
@@ -34,6 +35,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <inheritdoc />
|
||||
public string Description => _windowType == RateLimitWindowType.Decay ? $"Endpoint limit of {_limit} with a decay rate of {_decayRate}" : $"Limit of {_limit} per {_period}";
|
||||
|
||||
/// <summary>
|
||||
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
|
||||
/// </summary>
|
||||
public TimeSpan SafetyMargin => _safetyMargin;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -41,6 +47,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
int limit,
|
||||
TimeSpan period,
|
||||
RateLimitWindowType windowType,
|
||||
TimeSpan? safetyMargin = null,
|
||||
double? decayRate = null,
|
||||
Func<RequestDefinition, string?, string>? keySelector = null)
|
||||
{
|
||||
@@ -48,6 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
_period = period;
|
||||
_windowType = windowType;
|
||||
_decayRate = decayRate;
|
||||
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(period);
|
||||
_keySelector = keySelector ?? Default;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
}
|
||||
@@ -84,8 +92,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker()
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period, _safetyMargin) :
|
||||
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
|
||||
private int _currentWeight = 0;
|
||||
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;
|
||||
TimePeriod = period;
|
||||
DecreaseRate = decayRate;
|
||||
@@ -31,83 +40,118 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
if (amount == null)
|
||||
{
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_currentWeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
ResetState();
|
||||
return;
|
||||
}
|
||||
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
if (_currentWeight == 0)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Decrease the counter based on the last update time and decay rate
|
||||
DecreaseCounter(DateTime.UtcNow);
|
||||
var now = 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 (Current == 0)
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
throw new Exception("Request limit reached max utilization. " +
|
||||
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"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}");
|
||||
}
|
||||
throw new Exception(
|
||||
"Request limit reached max utilization. " +
|
||||
"This request can never execute with the current rate limiter configuration. " +
|
||||
$"Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"Request ratio: {(Current + weight) / (double)Limit}, " +
|
||||
$"AllowedRateRatio: {allowedRateRatio}");
|
||||
}
|
||||
|
||||
// Determine the time to wait before this weight can be applied without going over the rate limit
|
||||
return DetermineWaitTime(weight, allowedRateRatio);
|
||||
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}");
|
||||
}
|
||||
|
||||
// Weight can fit without going over limit
|
||||
return TimeSpan.Zero;
|
||||
return DetermineWaitTime(
|
||||
weight,
|
||||
allowedRateRatio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyWeight(int weight)
|
||||
{
|
||||
if (_currentWeight == 0)
|
||||
{
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_decayProgress = 0;
|
||||
}
|
||||
|
||||
_currentWeight += weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrease the counter based on time passed since last update and the decay rate
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
protected void DecreaseCounter(DateTime time)
|
||||
private void DecreaseCounter(DateTime now)
|
||||
{
|
||||
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate;
|
||||
var decrease = (int)Math.Floor(dif);
|
||||
if (decrease >= 1)
|
||||
if (_currentWeight == 0)
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif));
|
||||
_lastDecrease = time;
|
||||
_lastDecrease = now;
|
||||
_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>
|
||||
/// Determine the time to wait before the weight would fit
|
||||
/// </summary>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <param name="allowedRateRatio"></param>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime(int requestWeight, double allowedRateRatio)
|
||||
private TimeSpan DetermineWaitTime(
|
||||
int requestWeight,
|
||||
double allowedRateRatio)
|
||||
{
|
||||
var weightToRemove = Math.Max(Current + requestWeight - Limit * allowedRateRatio, 0);
|
||||
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
var weightToRemove = Current + requestWeight - Limit * allowedRateRatio;
|
||||
|
||||
// The counter is integer-valued, so enough whole weight units
|
||||
// must decay before the request can be admitted.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private int _currentWeight = 0;
|
||||
private DateTime? _nextReset;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for time offset between server and client
|
||||
/// </summary>
|
||||
private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public FixedAfterStartWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedAfterStartWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -57,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
if (_nextReset != null && checkTime > _nextReset)
|
||||
if (_nextReset != null && checkTime >= _nextReset + _safetyMargin)
|
||||
RemoveBefore(_nextReset.Value);
|
||||
|
||||
if (Current == 0)
|
||||
@@ -130,7 +133,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private TimeSpan DetermineWaitTime()
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||
var result = _nextReset!.Value + _safetyMargin - checkTime;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private readonly Queue<LimitEntry> _entries;
|
||||
private int _currentWeight = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for time offset between server and client
|
||||
/// </summary>
|
||||
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public FixedWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -56,7 +59,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)));
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var resetBoundary = checkTime >= startCurrentWindow + _safetyMargin
|
||||
? startCurrentWindow
|
||||
: startCurrentWindow - TimePeriod;
|
||||
RemoveBefore(resetBoundary);
|
||||
|
||||
if ((Current + weight) / (double)Limit > allowedRateRatio)
|
||||
{
|
||||
@@ -124,8 +131,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||
var result = wait.Add(_fixedWindowBuffer);
|
||||
var result = checkTime < startCurrentWindow + _safetyMargin
|
||||
? startCurrentWindow + _safetyMargin - checkTime
|
||||
: startCurrentWindow + TimePeriod + _safetyMargin - checkTime;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
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);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new List<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -56,7 +59,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
RemoveBefore(DateTime.UtcNow - TimePeriod);
|
||||
RemoveBefore(DateTime.UtcNow - TimePeriod - _safetyMargin);
|
||||
|
||||
if ((Current + weight) / (double)Limit > allowedRateRatio)
|
||||
{
|
||||
@@ -99,7 +102,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i].Timestamp < time)
|
||||
if (_entries[i].Timestamp <= time)
|
||||
{
|
||||
var entry = _entries[i];
|
||||
_entries.Remove(entry);
|
||||
@@ -127,7 +130,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
removedWeight += entry.Weight;
|
||||
if (removedWeight >= weightToRemove)
|
||||
{
|
||||
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||
var result = entry.Timestamp + TimePeriod + _safetyMargin - DateTime.UtcNow;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
internal static class WindowTrackerHelpers
|
||||
{
|
||||
private static readonly TimeSpan _minimumSafetyMargin = TimeSpan.FromMilliseconds(10);
|
||||
private static readonly TimeSpan _maximumSafetyMargin = TimeSpan.FromMilliseconds(250);
|
||||
|
||||
public static TimeSpan GetDefaultSafetyMargin(TimeSpan period)
|
||||
{
|
||||
var margin = TimeSpan.FromTicks(period.Ticks / 20);
|
||||
if (margin < _minimumSafetyMargin)
|
||||
return _minimumSafetyMargin;
|
||||
if (margin > _maximumSafetyMargin)
|
||||
return _maximumSafetyMargin;
|
||||
return margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get all futures symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset">Asset, for example `ETH`</param>
|
||||
Task<IExchangeCallResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
|
||||
Task<ExchangeCallResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a futures symbol
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Gets whether the client supports a spot symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
Task<IExchangeCallResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
|
||||
Task<ExchangeCallResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a spot symbol
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<IExchangeCallResultSharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user