1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 16:06:15 +00:00

feature: Handle error 429 when connecting websocket (#213)

* feature: Handle error 429 when connecting websocket

* Add preprocessor directive for NET6_0_OR_GREATER when checking for connection rate limit
This commit is contained in:
Jonnern 2024-09-24 12:50:59 +02:00 committed by GitHub
parent fee18fd183
commit 5d3de52da6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 94 additions and 5 deletions

View File

@ -519,6 +519,7 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!); var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address); var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage; socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection; socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
foreach (var ptg in PeriodicTaskRegistrations) foreach (var ptg in PeriodicTaskRegistrations)
@ -538,6 +539,19 @@ namespace CryptoExchange.Net.Clients
{ {
} }
/// <summary>
/// Process connect rate limited
/// </summary>
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
{
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
_logger.AddingRetryAfterGuard(retryAfter);
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
}
}
/// <summary> /// <summary>
/// Connect a socket /// Connect a socket
/// </summary> /// </summary>

View File

@ -27,6 +27,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
event Func<int, Task>? OnRequestRateLimited; event Func<int, Task>? OnRequestRateLimited;
/// <summary> /// <summary>
/// Connection was ratelimited and couldn't be established
/// </summary>
event Func<Task>? OnConnectRateLimited;
/// <summary>
/// Websocket error event /// Websocket error event
/// </summary> /// </summary>
event Func<Exception, Task> OnError; event Func<Exception, Task> OnError;

View File

@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, Exception?> _disposingSocketClient; private static readonly Action<ILogger, Exception?> _disposingSocketClient;
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription; private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections; private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
static SocketApiClientLoggingExtension() static SocketApiClientLoggingExtension()
{ {
@ -104,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information, LogLevel.Information,
new EventId(3017, "ReconnectingAll"), new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections"); "Reconnecting all {ConnectionCount} connections");
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
} }
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId) public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@ -185,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_reconnectingAllConnections(logger, connectionCount, null); _reconnectingAllConnections(logger, connectionCount, null);
} }
public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
} }
} }

View File

@ -47,6 +47,12 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero; public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
/// <summary>
/// This delay is used to set a RetryAfter guard on the connection after a rate limit is hit on the server.
/// This is used to prevent the client from reconnecting too quickly after a rate limit is hit.
/// </summary>
public TimeSpan? ConnectDelayAfterRateLimited { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Create a copy of this options
/// </summary> /// </summary>

View File

@ -21,25 +21,35 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "RetryAfterGuard"; public string Name => "RetryAfterGuard";
/// <inheritdoc /> /// <inheritdoc />
public string Description => $"Pause requests until after {After}"; public string Description => $"Pause {Type} until after {After}";
/// <summary> /// <summary>
/// The timestamp after which requests are allowed again /// The timestamp after which requests are allowed again
/// </summary> /// </summary>
public DateTime After { get; private set; } public DateTime After { get; private set; }
/// <summary>
/// The type of rate limit item this guard is for
/// </summary>
public RateLimitItemType Type { get; private set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="after"></param> /// <param name="after"></param>
public RetryAfterGuard(DateTime after) /// <param name="type"></param>
public RetryAfterGuard(DateTime after, RateLimitItemType type)
{ {
After = after; After = after;
Type = type;
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{ {
if (type != Type)
return LimitCheck.NotApplicable;
var dif = (After + _windowBuffer) - DateTime.UtcNow; var dif = (After + _windowBuffer) - DateTime.UtcNow;
if (dif <= TimeSpan.Zero) if (dif <= TimeSpan.Zero)
return LimitCheck.NotApplicable; return LimitCheck.NotApplicable;

View File

@ -29,8 +29,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified /// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
/// </summary> /// </summary>
/// <param name="retryAfter">The time after which requests can be send again</param> /// <param name="retryAfter">The time after which requests can be send again</param>
/// <param name="type">RateLimitType</param>
/// <returns></returns> /// <returns></returns>
Task SetRetryAfterGuardAsync(DateTime retryAfter); Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request);
/// <summary> /// <summary>
/// Returns the 'retry after' timestamp if set /// Returns the 'retry after' timestamp if set

View File

@ -152,7 +152,7 @@ namespace CryptoExchange.Net.RateLimiting
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task SetRetryAfterGuardAsync(DateTime retryAfter) public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type)
{ {
await _semaphore.WaitAsync().ConfigureAwait(false); await _semaphore.WaitAsync().ConfigureAwait(false);
@ -160,7 +160,7 @@ namespace CryptoExchange.Net.RateLimiting
{ {
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault(); var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
if (retryAfterGuard == null) if (retryAfterGuard == null)
_guards.Add(new RetryAfterGuard(retryAfter)); _guards.Add(new RetryAfterGuard(retryAfter, type));
else else
retryAfterGuard.UpdateAfter(retryAfter); retryAfterGuard.UpdateAfter(retryAfter);
} }

View File

@ -116,6 +116,9 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc /> /// <inheritdoc />
public event Func<int, Task>? OnRequestRateLimited; public event Func<int, Task>? OnRequestRateLimited;
/// <inheritdoc />
public event Func<Task>? OnConnectRateLimited;
/// <inheritdoc /> /// <inheritdoc />
public event Func<Exception, Task>? OnError; public event Func<Exception, Task>? OnError;
@ -186,6 +189,9 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy); SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true;
#endif
} }
catch (PlatformNotSupportedException) catch (PlatformNotSupportedException)
{ {
@ -220,6 +226,26 @@ namespace CryptoExchange.Net.Sockets
// if _ctsSource was canceled this was already logged // if _ctsSource was canceled this was already logged
_logger.SocketConnectionFailed(Id, e.Message, e); _logger.SocketConnectionFailed(Id, e.Message, e);
} }
if (e is WebSocketException we)
{
#if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message));
}
#else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead
if (we.Message.Contains("429"))
{
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message));
}
#endif
}
return new CallResult(new CantConnectError()); return new CallResult(new CantConnectError());
} }

View File

@ -71,6 +71,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action<IMessageAccessor>? UnhandledMessage; public event Action<IMessageAccessor>? UnhandledMessage;
/// <summary>
/// Connection was rate limited and couldn't be established
/// </summary>
public Func<Task>? ConnectRateLimitedAsync;
/// <summary> /// <summary>
/// The amount of subscriptions on this connection /// The amount of subscriptions on this connection
/// </summary> /// </summary>
@ -222,6 +227,7 @@ namespace CryptoExchange.Net.Sockets
_socket.OnStreamMessage += HandleStreamMessage; _socket.OnStreamMessage += HandleStreamMessage;
_socket.OnRequestSent += HandleRequestSentAsync; _socket.OnRequestSent += HandleRequestSentAsync;
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync; _socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
_socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
_socket.OnOpen += HandleOpenAsync; _socket.OnOpen += HandleOpenAsync;
_socket.OnClose += HandleCloseAsync; _socket.OnClose += HandleCloseAsync;
_socket.OnReconnecting += HandleReconnectingAsync; _socket.OnReconnecting += HandleReconnectingAsync;
@ -385,6 +391,16 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Handler for whenever a connection was rate limited and couldn't be established
/// </summary>
/// <returns></returns>
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ConnectRateLimitedAsync is not null)
await ConnectRateLimitedAsync().ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Handler for whenever a request is sent over the websocket /// Handler for whenever a request is sent over the websocket
/// </summary> /// </summary>

View File

@ -20,6 +20,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public event Func<Task>? OnReconnected; public event Func<Task>? OnReconnected;
public event Func<Task>? OnReconnecting; public event Func<Task>? OnReconnecting;
public event Func<int, Task>? OnRequestRateLimited; public event Func<int, Task>? OnRequestRateLimited;
public event Func<Task>? OnConnectRateLimited;
public event Func<Exception, Task>? OnError; public event Func<Exception, Task>? OnError;
#pragma warning restore 0067 #pragma warning restore 0067
public event Func<int, Task>? OnRequestSent; public event Func<int, Task>? OnRequestSent;