diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs
index 0d6e6b52..1793f7f6 100644
--- a/CryptoExchange.Net/Clients/BaseApiClient.cs
+++ b/CryptoExchange.Net/Clients/BaseApiClient.cs
@@ -1,10 +1,13 @@
-using System;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
+using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients
{
@@ -78,6 +81,11 @@ namespace CryptoExchange.Net.Clients
///
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
+ ///
+ /// Current rate limit admission override
+ ///
+ protected readonly AsyncLocal AdmissionOverride = new();
+
///
/// ctor
///
@@ -122,6 +130,29 @@ namespace CryptoExchange.Net.Clients
///
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
+ ///
+ public async Task WithRateLimitAdmissionAsync(
+ RateLimitAdmission admission,
+ Func> operation)
+ {
+ if (admission == null)
+ throw new ArgumentNullException(nameof(admission));
+ if (operation == null)
+ throw new ArgumentNullException(nameof(operation));
+
+ var previous = AdmissionOverride.Value;
+ AdmissionOverride.Value = admission;
+
+ try
+ {
+ return await operation().ConfigureAwait(false);
+ }
+ finally
+ {
+ AdmissionOverride.Value = previous;
+ }
+ }
+
///
/// Dispose
///
diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs
index 5ef88e58..b6973641 100644
--- a/CryptoExchange.Net/Clients/RestApiClient.cs
+++ b/CryptoExchange.Net/Clients/RestApiClient.cs
@@ -350,9 +350,8 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
- var rateRatio = 1.0;
- if (ClientOptions.RateLimitAdmission != null)
- rateRatio = ClientOptions.RateLimitAdmission(definition, requestWeight).MaxUtilization;
+ var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, requestWeight);
+ var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
var limitResult = await definition.RateLimitGate.ProcessAsync(
_logger,
@@ -379,9 +378,8 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
var singleRequestWeight = weightSingleLimiter ?? 1;
- var rateRatio = 1.0;
- if (ClientOptions.RateLimitAdmission != null)
- rateRatio = ClientOptions.RateLimitAdmission(definition, singleRequestWeight).MaxUtilization;
+ var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, singleRequestWeight);
+ var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
_logger,
diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs
index e0b14aaf..70f862b8 100644
--- a/CryptoExchange.Net/Clients/SocketApiClient.cs
+++ b/CryptoExchange.Net/Clients/SocketApiClient.cs
@@ -858,7 +858,8 @@ namespace CryptoExchange.Net.Clients
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
- RateLimitAdmission = ClientOptions.RateLimitAdmission,
+ RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
+ RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs
index 38db8177..62643872 100644
--- a/CryptoExchange.Net/ExtensionMethods.cs
+++ b/CryptoExchange.Net/ExtensionMethods.cs
@@ -1,5 +1,8 @@
-using CryptoExchange.Net.Interfaces;
+using CryptoExchange.Net.Clients;
+using CryptoExchange.Net.Interfaces;
+using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
+using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using System;
@@ -11,6 +14,7 @@ using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
+using System.Threading.Tasks;
using System.Web;
namespace CryptoExchange.Net
@@ -510,6 +514,23 @@ namespace CryptoExchange.Net
return bytes;
}
+
+ ///
+ /// Execute an operation with a specific rate limit admission rule
+ ///
+ /// Client
+ /// Rate limit admission rule
+ /// Operation to execute
+ public static Task WithRateLimitAdmissionAsync(
+ this TClient client,
+ RateLimitAdmission admission,
+ Func> operation)
+ where TClient : IRateLimitAdmissionClient
+ {
+ return client.WithRateLimitAdmissionAsync(
+ admission,
+ () => operation(client));
+ }
}
}
diff --git a/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs
index de5f88e9..ed9c80b6 100644
--- a/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs
+++ b/CryptoExchange.Net/Interfaces/Clients/IBaseApiClient.cs
@@ -1,6 +1,4 @@
-using CryptoExchange.Net.Authentication;
-using CryptoExchange.Net.Objects.Options;
-using CryptoExchange.Net.SharedApis;
+using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces.Clients
@@ -8,7 +6,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
///
/// Base api client
///
- public interface IBaseApiClient
+ public interface IBaseApiClient : IRateLimitAdmissionClient
{
///
/// Exchange name
diff --git a/CryptoExchange.Net/Interfaces/IRateLimitAdmissionClient.cs b/CryptoExchange.Net/Interfaces/IRateLimitAdmissionClient.cs
new file mode 100644
index 00000000..81eacae0
--- /dev/null
+++ b/CryptoExchange.Net/Interfaces/IRateLimitAdmissionClient.cs
@@ -0,0 +1,22 @@
+using CryptoExchange.Net.RateLimiting;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CryptoExchange.Net.Interfaces
+{
+ ///
+ /// A client supporting rate limit admission rules
+ ///
+ public interface IRateLimitAdmissionClient
+ {
+ ///
+ /// Execute an operation with a specific rate limit admission rule
+ ///
+ Task WithRateLimitAdmissionAsync(
+ RateLimitAdmission admission,
+ Func> operation);
+ }
+
+}
diff --git a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs
index 621a3662..bce85e6d 100644
--- a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs
+++ b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs
@@ -69,7 +69,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
///
- public Func? RateLimitAdmission { get; set; }
+ public Func? RateLimitAdmissionCallbackOptions { get; set; }
+
+ ///
+ /// A delegate for retrieving the current rate limit admission decision for a specific request. This allows for custom rate limiting logic to be implemented.
+ ///
+ public Func? RateLimitAdmissionCallbackRequest { get; set; }
///
/// Encoding for sending/receiving data
diff --git a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs
index 028455e4..5c3553c6 100644
--- a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs
+++ b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "EndpointLimitGuard";
///
- public string Description => $"Limit requests to endpoint";
+ public string Description => _windowType == RateLimitWindowType.Decay ? $"Endpoint limit of {_limit} with a decay rate of {_decayRate}" : $"Limit of {_limit} per {_period}";
///
/// ctor
diff --git a/CryptoExchange.Net/RateLimiting/RateLimitAdmission.cs b/CryptoExchange.Net/RateLimiting/RateLimitAdmission.cs
index 3f69f24c..272308d0 100644
--- a/CryptoExchange.Net/RateLimiting/RateLimitAdmission.cs
+++ b/CryptoExchange.Net/RateLimiting/RateLimitAdmission.cs
@@ -12,13 +12,13 @@ namespace CryptoExchange.Net.RateLimiting
///
/// Value ratio between 0 and 1
///
- public double MaxUtilization { get; }
+ public double MaxUtilizationRatio { get; }
private RateLimitAdmission(double maxUtilizationValue) {
if (maxUtilizationValue <= 0 || maxUtilizationValue > 1)
throw new ArgumentOutOfRangeException(nameof(maxUtilizationValue), "Max utilization value must be bigger than 0 and less than or equal to 1");
- MaxUtilization = maxUtilizationValue;
+ MaxUtilizationRatio = maxUtilizationValue;
}
///
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.RateLimiting
/// 1 means it's allowed to use the full rate limit.
///
/// 0.5 means a max use 50% of the rate limit, 1 means the request is allowed to use the full rate limit
- public static RateLimitAdmission WithMaxUtilization(double value)
+ public static RateLimitAdmission WithMaxUtilizationRatio(double value)
=> new RateLimitAdmission(value);
}
}
diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
index ac4c7558..aa9b99e0 100644
--- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
+++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
@@ -131,7 +131,10 @@ namespace CryptoExchange.Net.RateLimiting
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
- return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
+ var errorMsg = $"Rate limit check failed on guard {guard.Name}; {guard.Description}";
+ if (allowedRateRatio < 1)
+ errorMsg += $" with allowed ratio {allowedRateRatio}";
+ return CallResult.Fail(new ClientRateLimitError(errorMsg));
}
if (result.Delay != TimeSpan.Zero)
@@ -139,7 +142,14 @@ namespace CryptoExchange.Net.RateLimiting
// 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}";
+ var description =
+ result.Limit == null
+ ? guard.Description
+ : $"{guard.Description}, Request weight: {requestWeight}," +
+ $" Current: {result.Current}" +
+ $", Limit: {result.Limit}" +
+ (allowedRateRatio < 1 ? $", Allowed ratio: {allowedRateRatio}" : "") +
+ $", requests now being limited: {_waitingCount}";
if (type == RateLimitItemType.Connection)
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
else
diff --git a/CryptoExchange.Net/SharedApis/SharedApiBase.cs b/CryptoExchange.Net/SharedApis/SharedApiBase.cs
index d3ca516d..42192d2e 100644
--- a/CryptoExchange.Net/SharedApis/SharedApiBase.cs
+++ b/CryptoExchange.Net/SharedApis/SharedApiBase.cs
@@ -1,7 +1,10 @@
using CryptoExchange.Net.Clients;
+using CryptoExchange.Net.Interfaces.Clients;
+using CryptoExchange.Net.RateLimiting;
using System;
using System.Collections.Generic;
using System.Text;
+using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
@@ -12,7 +15,7 @@ namespace CryptoExchange.Net.SharedApis
{
private readonly Func _symbolFormatter;
private readonly Func _authDelegate;
-
+ private readonly IBaseApiClient _apiClient;
private IReadOnlyCollection _capabilities = Array.Empty();
IReadOnlyCollection ISharedApi.Capabilities => _capabilities;
@@ -33,16 +36,17 @@ namespace CryptoExchange.Net.SharedApis
///
public SharedApiBase(
SharedTransport transport,
- string exchange,
+ IBaseApiClient apiClient,
TradingMode[] supportedTradingModes,
Func authenticated,
Func formatSymbol)
{
- Transport = transport;
- Exchange = exchange;
- SupportedTradingModes = supportedTradingModes;
+ _apiClient = apiClient;
_authDelegate = authenticated;
_symbolFormatter = formatSymbol;
+ Transport = transport;
+ Exchange = apiClient.Exchange;
+ SupportedTradingModes = supportedTradingModes;
}
///
@@ -64,6 +68,12 @@ namespace CryptoExchange.Net.SharedApis
///
public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticExchangeParameters(Exchange);
+ ///
+ public Task WithRateLimitAdmissionAsync(
+ RateLimitAdmission admission,
+ Func> operation)
+ => _apiClient.WithRateLimitAdmissionAsync(admission, operation);
+
///
public abstract SharedClientInfo Discover();
}
diff --git a/CryptoExchange.Net/SharedApis/V2/ISharedApi.cs b/CryptoExchange.Net/SharedApis/V2/ISharedApi.cs
index d3958fad..d32c7de9 100644
--- a/CryptoExchange.Net/SharedApis/V2/ISharedApi.cs
+++ b/CryptoExchange.Net/SharedApis/V2/ISharedApi.cs
@@ -1,12 +1,15 @@
-using System;
+using CryptoExchange.Net.Interfaces;
+using CryptoExchange.Net.RateLimiting;
+using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
///
/// Shared API
///
- public interface ISharedApi
+ public interface ISharedApi : IRateLimitAdmissionClient
{
///
/// Name of the exchange
diff --git a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs
index d28b27f5..ce119f62 100644
--- a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs
+++ b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs
@@ -208,11 +208,17 @@ namespace CryptoExchange.Net.Sockets.Default
{
if (Parameters.RateLimiter != null)
{
- var rateRatio = 1.0;
- if (Parameters.RateLimitAdmission != null)
- rateRatio = Parameters.RateLimitAdmission(_requestDefinition, 1).MaxUtilization;
-
- var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, _requestDefinition, null, 1, Parameters.RateLimitingBehavior, null, rateRatio, _ctsSource.Token).ConfigureAwait(false);
+ var limitResult = await Parameters.RateLimiter.ProcessAsync(
+ _logger,
+ Id,
+ RateLimitItemType.Connection,
+ _requestDefinition,
+ null,
+ 1,
+ Parameters.RateLimitingBehavior,
+ null,
+ 1.0,
+ _ctsSource.Token).ConfigureAwait(false);
if (!limitResult.Success)
return CallResult.Fail(new ClientRateLimitError("Connection limit reached"));
}
@@ -386,7 +392,15 @@ namespace CryptoExchange.Net.Sockets.Default
var bytes = Parameters.Encoding.GetBytes(data);
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
- _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Text, Weight = weight, Bytes = bytes });
+ _sendBuffer.Enqueue(
+ new SendItem
+ {
+ Id = id,
+ Type = WebSocketMessageType.Text,
+ Weight = weight,
+ Bytes = bytes,
+ RateLimitAdmission = Parameters.RateLimitAdmissionCallbackRequest?.Invoke()
+ });
_sendEvent.Set();
return true;
}
@@ -398,7 +412,14 @@ namespace CryptoExchange.Net.Sockets.Default
return false;
_logger.SocketAddingBytesToSendBuffer(Id, id, data);
- _sendBuffer.Enqueue(new SendItem { Id = id, Type = WebSocketMessageType.Binary, Weight = weight, Bytes = data });
+ _sendBuffer.Enqueue(
+ new SendItem {
+ Id = id,
+ Type = WebSocketMessageType.Binary,
+ Weight = weight,
+ Bytes = data,
+ RateLimitAdmission = Parameters.RateLimitAdmissionCallbackRequest?.Invoke()
+ });
_sendEvent.Set();
return true;
}
@@ -534,9 +555,8 @@ namespace CryptoExchange.Net.Sockets.Default
{
if (Parameters.RateLimiter != null)
{
- var rateRatio = 1.0;
- if (Parameters.RateLimitAdmission != null)
- rateRatio = Parameters.RateLimitAdmission(_requestDefinition, data.Weight).MaxUtilization;
+ var admission = data.RateLimitAdmission ?? Parameters.RateLimitAdmissionCallbackOptions?.Invoke(_requestDefinition, data.Weight);
+ var rateRatio = admission?.MaxUtilizationRatio ?? 1.0;
try
{
@@ -1035,6 +1055,11 @@ namespace CryptoExchange.Net.Sockets.Default
///
public WebSocketMessageType Type { get; set; }
+ ///
+ /// Rate limit admission override
+ ///
+ public RateLimitAdmission? RateLimitAdmission { get; set; }
+
///
/// The bytes to send
///