using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
///
/// Limits the amount of requests to a certain constraint
///
public class RateLimiter : IRateLimiter
{
private readonly object _limiterLock = new object();
internal List Limiters = new List();
///
/// Create a new RateLimiter. Configure the rate limiter by calling ,
/// , or .
///
public RateLimiter()
{
}
///
/// Add a rate limit for the total amount of requests per time period
///
/// The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1
/// The time period the limit is for
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{
lock(_limiterLock)
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this;
}
///
/// Add a rate lmit for the amount of requests per time for an endpoint
///
/// The endpoint the limit is for
/// The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1
/// The time period the limit is for
/// The HttpMethod the limit is for, null for all
/// If set to true it ignores other rate limits
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
///
/// Add a rate lmit for the amount of requests per time for an endpoint
///
/// The endpoints the limit is for
/// The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1
/// The time period the limit is for
/// The HttpMethod the limit is for, null for all
/// If set to true it ignores other rate limits
public RateLimiter AddEndpointLimit(IEnumerable endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
///
/// Add a rate lmit for the amount of requests per time for an endpoint
///
/// The endpoint the limit is for
/// The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1
/// The time period the limit is for
/// The HttpMethod the limit is for, null for all
/// If set to true it ignores other rate limits
/// Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
{
lock(_limiterLock)
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
return this;
}
///
/// Add a rate limit for the amount of requests per Api key
///
/// The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1
/// The time period the limit is for
/// Only include calls that are signed in this limiter
/// Exclude requests with API key from the total rate limiter
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{
lock(_limiterLock)
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
return this;
}
///
public async Task> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
{
int totalWaitTime = 0;
EndpointRateLimiter? endpointLimit;
lock (_limiterLock)
endpointLimit = Limiters.OfType().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
if(endpointLimit != null)
{
var waitResult = await ProcessTopic(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
if (endpointLimit?.IgnoreOtherRateLimits == true)
return new CallResult(totalWaitTime);
List partialEndpointLimits;
lock (_limiterLock)
partialEndpointLimits = Limiters.OfType().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
foreach (var partialEndpointLimit in partialEndpointLimits)
{
if (partialEndpointLimit.CountPerEndpoint)
{
SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock)
{
thisEndpointLimit = Limiters.OfType().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
if (thisEndpointLimit == null)
{
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
Limiters.Add(thisEndpointLimit);
}
}
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
else
{
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
return new CallResult(totalWaitTime);
ApiKeyRateLimiter? apiLimit;
lock (_limiterLock)
apiLimit = Limiters.OfType().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
if (apiLimit != null)
{
if(apiKey == null)
{
if (!apiLimit.OnlyForSignedRequests)
{
var waitResult = await ProcessTopic(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
else if (signed || !apiLimit.OnlyForSignedRequests)
{
SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock)
{
thisApiLimit = Limiters.OfType().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
if (thisApiLimit == null)
{
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
Limiters.Add(thisApiLimit);
}
}
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
return new CallResult(totalWaitTime);
TotalRateLimiter? totalLimit;
lock (_limiterLock)
totalLimit = Limiters.OfType().SingleOrDefault();
if (totalLimit != null)
{
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
return new CallResult(totalWaitTime);
}
private static async Task> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
{
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult(new CancellationRequestedError());
}
sw.Stop();
int totalWaitTime = 0;
while (true)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
for (var i = 0; i < historyTopic.Entries.Count; i++)
{
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
{
historyTopic.Entries.Remove(historyTopic.Entries[i]);
i--;
}
else
break;
}
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
if (currentWeight + requestWeight > historyTopic.Limit)
{
if (currentWeight == 0)
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
// Wait until the next entry should be removed from the history
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
if (thisWaitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
{
historyTopic.Semaphore.Release();
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
log.Write(LogLevel.Warning, msg);
return new CallResult(new RateLimitError(msg));
}
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
try
{
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult(new CancellationRequestedError());
}
totalWaitTime += thisWaitTime;
}
}
else
{
break;
}
}
var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
historyTopic.Semaphore.Release();
return new CallResult(totalWaitTime);
}
internal struct LimitEntry
{
public DateTime Timestamp { get; set; }
public int Weight { get; set; }
public LimitEntry(DateTime timestamp, int weight)
{
Timestamp = timestamp;
Weight = weight;
}
}
internal class Limiter
{
public RateLimitType Type { get; set; }
public HttpMethod? Method { get; set; }
public SemaphoreSlim Semaphore { get; set; }
public int Limit { get; set; }
public TimeSpan Period { get; set; }
public List Entries { get; set; } = new List();
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
{
Semaphore = new SemaphoreSlim(1, 1);
Type = type;
Limit = limit;
Period = perPeriod;
Method = method;
}
}
internal class TotalRateLimiter : Limiter
{
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
: base(RateLimitType.Total, limit, perPeriod, method)
{
}
public override string ToString()
{
return nameof(TotalRateLimiter);
}
}
internal class EndpointRateLimiter: Limiter
{
public string[] Endpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
:base(RateLimitType.Endpoint, limit, perPeriod, method)
{
Endpoints = endpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
}
public override string ToString()
{
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
}
}
internal class PartialEndpointRateLimiter : Limiter
{
public string[] PartialEndpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public bool CountPerEndpoint { get; set; }
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
{
PartialEndpoints = partialEndpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
CountPerEndpoint = countPerEndpoint;
}
public override string ToString()
{
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
}
}
internal class ApiKeyRateLimiter : Limiter
{
public bool OnlyForSignedRequests { get; set; }
public bool IgnoreTotalRateLimit { get; set; }
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
:base(RateLimitType.ApiKey, limit, perPeriod, method)
{
OnlyForSignedRequests = onlyForSignedRequests;
IgnoreTotalRateLimit = ignoreTotalRateLimit;
}
}
internal class SingleTopicRateLimiter: Limiter
{
public object Topic { get; set; }
public SingleTopicRateLimiter(object topic, Limiter limiter)
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
{
Topic = topic;
}
public override string ToString()
{
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
}
}
internal enum RateLimitType
{
Total,
Endpoint,
PartialEndpoint,
ApiKey
}
}
}