1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-13 22:51:39 +00:00

Added WithRateLimitAdmissionAsync for REST and Socket clients

This commit is contained in:
Jkorf
2026-09-17 11:33:47 +02:00
parent e5da147ba8
commit a34684cd31
13 changed files with 161 additions and 37 deletions
+32 -1
View File
@@ -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
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// Current rate limit admission override
/// </summary>
protected readonly AsyncLocal<RateLimitAdmission?> AdmissionOverride = new();
/// <summary>
/// ctor
/// </summary>
@@ -122,6 +130,29 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public async Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> 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;
}
}
/// <summary>
/// Dispose
/// </summary>
+4 -6
View File
@@ -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,
@@ -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,
+22 -1
View File
@@ -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;
}
/// <summary>
/// Execute an operation with a specific rate limit admission rule
/// </summary>
/// <param name="client">Client</param>
/// <param name="admission">Rate limit admission rule</param>
/// <param name="operation">Operation to execute</param>
public static Task<TResult> WithRateLimitAdmissionAsync<TClient, TResult>(
this TClient client,
RateLimitAdmission admission,
Func<TClient, Task<TResult>> operation)
where TClient : IRateLimitAdmissionClient
{
return client.WithRateLimitAdmissionAsync(
admission,
() => operation(client));
}
}
}
@@ -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
/// <summary>
/// Base api client
/// </summary>
public interface IBaseApiClient
public interface IBaseApiClient : IRateLimitAdmissionClient
{
/// <summary>
/// Exchange name
@@ -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
{
/// <summary>
/// A client supporting rate limit admission rules
/// </summary>
public interface IRateLimitAdmissionClient
{
/// <summary>
/// Execute an operation with a specific rate limit admission rule
/// </summary>
Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation);
}
}
@@ -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.
/// </summary>
public Func<RequestDefinition, int, RateLimitAdmission>? RateLimitAdmission { get; set; }
public Func<RequestDefinition, int, RateLimitAdmission?>? RateLimitAdmissionCallbackOptions { get; set; }
/// <summary>
/// A delegate for retrieving the current rate limit admission decision for a specific request. This allows for custom rate limiting logic to be implemented.
/// </summary>
public Func<RateLimitAdmission?>? RateLimitAdmissionCallbackRequest { get; set; }
/// <summary>
/// Encoding for sending/receiving data
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "EndpointLimitGuard";
/// <inheritdoc />
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}";
/// <summary>
/// ctor
@@ -12,13 +12,13 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// Value ratio between 0 and 1
/// </summary>
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;
}
/// <summary>
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.RateLimiting
/// 1 means it's allowed to use the full rate limit.
/// </summary>
/// <param name="value">0.5 means a max use 50% of the rate limit, 1 means the request is allowed to use the full rate limit</param>
public static RateLimitAdmission WithMaxUtilization(double value)
public static RateLimitAdmission WithMaxUtilizationRatio(double value)
=> new RateLimitAdmission(value);
}
}
@@ -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
+15 -5
View File
@@ -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<string, string, TradingMode, DateTime?, string> _symbolFormatter;
private readonly Func<bool> _authDelegate;
private readonly IBaseApiClient _apiClient;
private IReadOnlyCollection<CapabilityOptions> _capabilities = Array.Empty<CapabilityOptions>();
IReadOnlyCollection<CapabilityOptions> ISharedApi.Capabilities => _capabilities;
@@ -33,16 +36,17 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public SharedApiBase(
SharedTransport transport,
string exchange,
IBaseApiClient apiClient,
TradingMode[] supportedTradingModes,
Func<bool> authenticated,
Func<string, string, TradingMode, DateTime?, string> formatSymbol)
{
Transport = transport;
Exchange = exchange;
SupportedTradingModes = supportedTradingModes;
_apiClient = apiClient;
_authDelegate = authenticated;
_symbolFormatter = formatSymbol;
Transport = transport;
Exchange = apiClient.Exchange;
SupportedTradingModes = supportedTradingModes;
}
/// <inheritdoc />
@@ -64,6 +68,12 @@ namespace CryptoExchange.Net.SharedApis
/// <inheritdoc />
public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticExchangeParameters(Exchange);
/// <inheritdoc />
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation)
=> _apiClient.WithRateLimitAdmissionAsync(admission, operation);
/// <inheritdoc />
public abstract SharedClientInfo Discover();
}
@@ -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
{
/// <summary>
/// Shared API
/// </summary>
public interface ISharedApi
public interface ISharedApi : IRateLimitAdmissionClient
{
/// <summary>
/// Name of the exchange
@@ -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
/// </summary>
public WebSocketMessageType Type { get; set; }
/// <summary>
/// Rate limit admission override
/// </summary>
public RateLimitAdmission? RateLimitAdmission { get; set; }
/// <summary>
/// The bytes to send
/// </summary>