mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-21 13:23:07 +00:00
Feature/ratelimit refactor (#197)
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether they're authenticated or not
|
||||
/// </summary>
|
||||
public class AuthenticatedEndpointFilter : IGuardFilter
|
||||
{
|
||||
private readonly bool _authenticated;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="authenticated"></param>
|
||||
public AuthenticatedEndpointFilter(bool authenticated)
|
||||
{
|
||||
_authenticated = authenticated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> definition.Authenticated == _authenticated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether the request path matches a specific path
|
||||
/// </summary>
|
||||
public class ExactPathFilter : IGuardFilter
|
||||
{
|
||||
private readonly string _path;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public ExactPathFilter(string path)
|
||||
{
|
||||
_path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether the request path matches any specific path in a list
|
||||
/// </summary>
|
||||
public class ExactPathsFilter : IGuardFilter
|
||||
{
|
||||
private readonly HashSet<string> _paths;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="paths"></param>
|
||||
public ExactPathsFilter(IEnumerable<string> paths)
|
||||
{
|
||||
_paths = new HashSet<string>(paths);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> _paths.Contains(definition.Path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether the host address matches a specific address
|
||||
/// </summary>
|
||||
public class HostFilter : IGuardFilter
|
||||
{
|
||||
private readonly string _host;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
public HostFilter(string host)
|
||||
{
|
||||
_host = host;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> host == _host;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether it's a connection or a request
|
||||
/// </summary>
|
||||
public class LimitItemTypeFilter : IGuardFilter
|
||||
{
|
||||
private readonly RateLimitItemType _type;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
public LimitItemTypeFilter(RateLimitItemType type)
|
||||
{
|
||||
_type = type;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> type == _type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on whether the path starts with a specific string
|
||||
/// </summary>
|
||||
public class PathStartFilter : IGuardFilter
|
||||
{
|
||||
private readonly string _path;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public PathStartFilter(string path)
|
||||
{
|
||||
_path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class RateLimitGuard : IRateLimitGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply guard per host
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
|
||||
/// <summary>
|
||||
/// Apply guard per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
||||
/// <summary>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
|
||||
/// <summary>
|
||||
/// Apply guard per API key per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
|
||||
|
||||
private readonly IEnumerable<IGuardFilter> _filters;
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private RateLimitWindowType _windowType;
|
||||
private double? _decayRate;
|
||||
private int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "RateLimitGuard";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _windowType == RateLimitWindowType.Decay ? $"Limit of {Limit} with a decay rate of {_decayRate}" : $"Limit of {Limit} per {TimeSpan}";
|
||||
|
||||
/// <summary>
|
||||
/// The limit per period
|
||||
/// </summary>
|
||||
public int Limit { get; }
|
||||
/// <summary>
|
||||
/// The time period for the limit
|
||||
/// </summary>
|
||||
public TimeSpan TimeSpan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="keySelector">The rate limit key selector</param>
|
||||
/// <param name="filter">Filter for rate limit items. Only when the rate limit item passes the filter the guard will apply</param>
|
||||
/// <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="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="keySelector">The rate limit key selector</param>
|
||||
/// <param name="filters">Filters for rate limit items. Only when the rate limit item passes all filters the guard will apply</param>
|
||||
/// <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="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
_windowType = windowType;
|
||||
Limit = limit;
|
||||
TimeSpan = timeSpan;
|
||||
_keySelector = keySelector;
|
||||
_decayRate = decayPerTimeSpan;
|
||||
_connectionWeight = connectionWeight;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
foreach(var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
return LimitCheck.NotApplicable;
|
||||
}
|
||||
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
var key = _keySelector(definition, host, apiKey);
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
return RateLimitState.NotApplied;
|
||||
}
|
||||
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
var key = _keySelector(definition, host, apiKey);
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new WindowTracker
|
||||
/// </summary>
|
||||
/// <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) :
|
||||
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
{
|
||||
/// <summary>
|
||||
/// Retry after guard
|
||||
/// </summary>
|
||||
public class RetryAfterGuard : IRateLimitGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for time offset between server and client
|
||||
/// </summary>
|
||||
private static readonly TimeSpan _windowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "RetryAfterGuard";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => $"Pause requests until after {After}";
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp after which requests are allowed again
|
||||
/// </summary>
|
||||
public DateTime After { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="after"></param>
|
||||
public RetryAfterGuard(DateTime after)
|
||||
{
|
||||
After = after;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
||||
if (dif <= TimeSpan.Zero)
|
||||
return LimitCheck.NotApplicable;
|
||||
|
||||
return LimitCheck.Needed(dif, default, default, default);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
return RateLimitState.NotApplied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the 'after' time
|
||||
/// </summary>
|
||||
/// <param name="after"></param>
|
||||
public void UpdateAfter(DateTime after) => After = after;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit guard for a per endpoint limit
|
||||
/// </summary>
|
||||
public class SingleLimitGuard : IRateLimitGuard
|
||||
{
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "EndpointLimitGuard";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => $"Limit requests to endpoint";
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
|
||||
{
|
||||
_windowType = windowType;
|
||||
_decayRate = decayRate;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
var key = definition.Path + definition.Method;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
|
||||
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
{
|
||||
var key = definition.Path + definition.Method + definition;
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new WindowTracker
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
|
||||
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter requests based on specific condition
|
||||
/// </summary>
|
||||
public interface IGuardFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a request or connection passes this filter
|
||||
/// </summary>
|
||||
/// <param name="type">The type of item</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <returns>True if passed</returns>
|
||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit gate
|
||||
/// </summary>
|
||||
public interface IRateLimitGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Event when the rate limit is triggered
|
||||
/// </summary>
|
||||
event Action<RateLimitEvent> RateLimitTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit guard
|
||||
/// </summary>
|
||||
/// <param name="guard">Guard to add</param>
|
||||
/// <returns></returns>
|
||||
IRateLimitGate AddGuard(IRateLimitGuard guard);
|
||||
|
||||
/// <summary>
|
||||
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
|
||||
/// </summary>
|
||||
/// <param name="retryAfter">The time after which requests can be send again</param>
|
||||
/// <returns></returns>
|
||||
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
||||
|
||||
/// <summary>
|
||||
/// Set the SingleLimitGuard for handling individual endpoint rate limits
|
||||
/// </summary>
|
||||
/// <param name="guard"></param>
|
||||
/// <returns></returns>
|
||||
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the 'retry after' timestamp if set
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<DateTime?> GetRetryAfterTime();
|
||||
|
||||
/// <summary>
|
||||
/// Process a request. Enforces the configured rate limits. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="itemId">Id of the item to check</param>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">Request weight</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="itemId">Id of the item to check</param>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">Request weight</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit guard
|
||||
/// </summary>
|
||||
public interface IRateLimitGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Description
|
||||
/// </summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a request can pass this rate limit guard
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <returns></returns>
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||
|
||||
/// <summary>
|
||||
/// Apply the request to this guard with the specified weight
|
||||
/// </summary>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <returns></returns>
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit window tracker
|
||||
/// </summary>
|
||||
public interface IWindowTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Time period the limit is for
|
||||
/// </summary>
|
||||
TimeSpan TimePeriod { get; }
|
||||
/// <summary>
|
||||
/// The limit in the time period
|
||||
/// </summary>
|
||||
int Limit { get; }
|
||||
/// <summary>
|
||||
/// The current count within the time period
|
||||
/// </summary>
|
||||
int Current { get; }
|
||||
/// <summary>
|
||||
/// Get the time to wait to fit the weight
|
||||
/// </summary>
|
||||
/// <param name="weight"></param>
|
||||
/// <returns></returns>
|
||||
TimeSpan GetWaitTime(int weight);
|
||||
/// <summary>
|
||||
/// Register the weight in this window
|
||||
/// </summary>
|
||||
/// <param name="weight">Request weight</param>
|
||||
void ApplyWeight(int weight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit check
|
||||
/// </summary>
|
||||
public readonly struct LimitCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Is guard applicable
|
||||
/// </summary>
|
||||
public bool Applicable { get; }
|
||||
/// <summary>
|
||||
/// Delay needed
|
||||
/// </summary>
|
||||
public TimeSpan Delay { get; }
|
||||
/// <summary>
|
||||
/// Current counter
|
||||
/// </summary>
|
||||
public int Current { get; }
|
||||
/// <summary>
|
||||
/// Limit
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
/// <summary>
|
||||
/// Time period
|
||||
/// </summary>
|
||||
public TimeSpan? Period { get; }
|
||||
|
||||
private LimitCheck(bool applicable, TimeSpan delay, int limit, TimeSpan period, int current)
|
||||
{
|
||||
Applicable = applicable;
|
||||
Delay = delay;
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
Current = current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not applicable
|
||||
/// </summary>
|
||||
public static LimitCheck NotApplicable { get; } = new LimitCheck(false, default, default, default, default);
|
||||
|
||||
/// <summary>
|
||||
/// No wait needed
|
||||
/// </summary>
|
||||
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
|
||||
|
||||
/// <summary>
|
||||
/// Wait needed
|
||||
/// </summary>
|
||||
/// <param name="delay">The delay needed</param>
|
||||
/// <param name="limit">Limit per period</param>
|
||||
/// <param name="period">Period the limit is for</param>
|
||||
/// <param name="current">Current counter</param>
|
||||
/// <returns></returns>
|
||||
public static LimitCheck Needed(TimeSpan delay, int limit, TimeSpan period, int current) => new(true, delay, limit, period, current);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// A rate limit entry
|
||||
/// </summary>
|
||||
public struct LimitEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Timestamp of the item
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
/// <summary>
|
||||
/// Item weight
|
||||
/// </summary>
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="timestamp"></param>
|
||||
/// <param name="weight"></param>
|
||||
public LimitEntry(DateTime timestamp, int weight)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Weight = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit event
|
||||
/// </summary>
|
||||
public record RateLimitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the API limit that is reached
|
||||
/// </summary>
|
||||
public string ApiLimit { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Description of the limit that is reached
|
||||
/// </summary>
|
||||
public string LimitDescription { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The request definition
|
||||
/// </summary>
|
||||
public RequestDefinition RequestDefinition { get; set; }
|
||||
/// <summary>
|
||||
/// The host the request is for
|
||||
/// </summary>
|
||||
public string Host { get; set; } = default!;
|
||||
/// <summary>
|
||||
/// The current counter value
|
||||
/// </summary>
|
||||
public int Current { get; set; }
|
||||
/// <summary>
|
||||
/// The weight of the limited request
|
||||
/// </summary>
|
||||
public int RequestWeight { get; set; }
|
||||
/// <summary>
|
||||
/// The limit per time period
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// The time period the limit is for
|
||||
/// </summary>
|
||||
public TimeSpan? TimePeriod { get; set; }
|
||||
/// <summary>
|
||||
/// The time the request will be delayed for if the Behaviour is RateLimitingBehaviour.Wait
|
||||
/// </summary>
|
||||
public TimeSpan? DelayTime { get; set; }
|
||||
/// <summary>
|
||||
/// The handling behaviour for the rquest
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour Behaviour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="apiLimit"></param>
|
||||
/// <param name="limitDescription"></param>
|
||||
/// <param name="definition"></param>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="current"></param>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="timePeriod"></param>
|
||||
/// <param name="delayTime"></param>
|
||||
/// <param name="behaviour"></param>
|
||||
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
{
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
RequestDefinition = definition;
|
||||
Host = host;
|
||||
Current = current;
|
||||
RequestWeight = requestWeight;
|
||||
Limit = limit;
|
||||
TimePeriod = timePeriod;
|
||||
DelayTime = delayTime;
|
||||
Behaviour = behaviour;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class RateLimitGate : IRateLimitGate
|
||||
{
|
||||
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
|
||||
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly string _name;
|
||||
|
||||
private int _waitingCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<RateLimitEvent>? RateLimitTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RateLimitGate(string name)
|
||||
{
|
||||
_name = name;
|
||||
_guards = new ConcurrentBag<IRateLimitGuard>();
|
||||
_semaphore = new SemaphoreSlim(1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_waitingCount--;
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
if (requestWeight == 0)
|
||||
requestWeight = 1;
|
||||
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_waitingCount--;
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
{
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
// Check if a wait is needed for this guard
|
||||
var result = guard.Check(type, definition, host, apiKey, requestWeight);
|
||||
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
// Delay is needed and limit behaviour is to fail the request
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitConnectionFailed(itemId, guard.Name, guard.Description);
|
||||
else
|
||||
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
}
|
||||
|
||||
if (result.Delay != TimeSpan.Zero)
|
||||
{
|
||||
// Delay is needed and limit behaviour is to wait for the request to be under the limit
|
||||
_semaphore.Release();
|
||||
|
||||
var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}";
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
|
||||
else
|
||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the weight on each guard
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
|
||||
if (result.IsApplied)
|
||||
{
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
|
||||
else
|
||||
logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current);
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRateLimitGate AddGuard(IRateLimitGuard guard)
|
||||
{
|
||||
_guards.Add(guard);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
|
||||
{
|
||||
_singleLimitGuard = guard;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
||||
{
|
||||
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
||||
if (retryAfterGuard == null)
|
||||
_guards.Add(new RetryAfterGuard(retryAfter));
|
||||
else
|
||||
retryAfterGuard.UpdateAfter(retryAfter);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DateTime?> GetRetryAfterTime()
|
||||
{
|
||||
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
||||
return retryAfterGuard?.After;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit item type
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum RateLimitItemType
|
||||
{
|
||||
/// <summary>
|
||||
/// A connection attempt
|
||||
/// </summary>
|
||||
Connection = 1,
|
||||
/// <summary>
|
||||
/// A request
|
||||
/// </summary>
|
||||
Request = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit state
|
||||
/// </summary>
|
||||
public struct RateLimitState
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit
|
||||
/// </summary>
|
||||
public int Limit { get; }
|
||||
/// <summary>
|
||||
/// Period
|
||||
/// </summary>
|
||||
public TimeSpan Period { get; }
|
||||
/// <summary>
|
||||
/// Current count
|
||||
/// </summary>
|
||||
public int Current { get; }
|
||||
/// <summary>
|
||||
/// Whether the limit is applied
|
||||
/// </summary>
|
||||
public bool IsApplied { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="applied"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="period"></param>
|
||||
/// <param name="current"></param>
|
||||
public RateLimitState(bool applied, int limit, TimeSpan period, int current)
|
||||
{
|
||||
IsApplied = applied;
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
Current = current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not applied result
|
||||
/// </summary>
|
||||
public static RateLimitState NotApplied { get; } = new RateLimitState(false, default, default, default);
|
||||
/// <summary>
|
||||
/// Applied result
|
||||
/// </summary>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="period"></param>
|
||||
/// <param name="current"></param>
|
||||
/// <returns></returns>
|
||||
public static RateLimitState Applied(int limit, TimeSpan period, int current) => new RateLimitState(true, limit, period, current);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
internal class DecayWindowTracker : IWindowTracker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeSpan TimePeriod { get; }
|
||||
/// <summary>
|
||||
/// Decrease rate per TimePeriod
|
||||
/// </summary>
|
||||
public double DecreaseRate { get; }
|
||||
/// <inheritdoc />
|
||||
public int Limit { get; }
|
||||
/// <inheritdoc />
|
||||
public int Current => _currentWeight;
|
||||
|
||||
private int _currentWeight = 0;
|
||||
private DateTime _lastDecrease = DateTime.UtcNow;
|
||||
|
||||
public DecayWindowTracker(int limit, TimeSpan period, double decayRate)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
DecreaseRate = decayRate;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
// Decrease the counter based on the last update time and decay rate
|
||||
DecreaseCounter(DateTime.UtcNow);
|
||||
|
||||
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)
|
||||
{
|
||||
if (_currentWeight == 0)
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_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)
|
||||
{
|
||||
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate;
|
||||
var decrease = (int)Math.Floor(dif);
|
||||
if (decrease >= 1)
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif));
|
||||
_lastDecrease = time;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the time to wait before the weight would fit
|
||||
/// </summary>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||
{
|
||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
internal class FixedAfterStartWindowTracker : IWindowTracker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeSpan TimePeriod { get; }
|
||||
/// <inheritdoc />
|
||||
public int Limit { get; }
|
||||
/// <inheritdoc />
|
||||
public int Current => _currentWeight;
|
||||
|
||||
private readonly Queue<LimitEntry> _entries;
|
||||
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);
|
||||
|
||||
public FixedAfterStartWindowTracker(int limit, TimeSpan period)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
if (_nextReset != null && checkTime > _nextReset)
|
||||
RemoveBefore(_nextReset.Value);
|
||||
|
||||
if (Current == 0)
|
||||
_nextReset = null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyWeight(int weight)
|
||||
{
|
||||
if (_currentWeight == 0)
|
||||
_nextReset = DateTime.UtcNow + TimePeriod;
|
||||
_currentWeight += weight;
|
||||
_entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove items before a certain time
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the time to wait before a new item would fit
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime()
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
internal class FixedWindowTracker : IWindowTracker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TimeSpan TimePeriod { get; }
|
||||
/// <inheritdoc />
|
||||
public int Limit { get; }
|
||||
/// <inheritdoc />
|
||||
public int Current => _currentWeight;
|
||||
|
||||
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);
|
||||
|
||||
public FixedWindowTracker(int limit, TimeSpan period)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyWeight(int weight)
|
||||
{
|
||||
_currentWeight += weight;
|
||||
_entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove items before a certain time
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the time to wait before a new item would fit
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime()
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||
return wait.Add(_fixedWindowBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_entries = new List<LimitEntry>();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Request not possible to execute with current rate limit guard. " +
|
||||
$" Request weight: {requestWeight}, Ratelimit: {Limit}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user