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 socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
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>
/// Connect a socket
/// </summary>

View File

@ -27,6 +27,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Func<int, Task>? OnRequestRateLimited;
/// <summary>
/// Connection was ratelimited and couldn't be established
/// </summary>
event Func<Task>? OnConnectRateLimited;
/// <summary>
/// Websocket error event
/// </summary>
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, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
static SocketApiClientLoggingExtension()
{
@ -104,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"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)
@ -185,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_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>
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>
/// Create a copy of this options
/// </summary>

View File

@ -21,25 +21,35 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "RetryAfterGuard";
/// <inheritdoc />
public string Description => $"Pause requests until after {After}";
public string Description => $"Pause {Type} until after {After}";
/// <summary>
/// The timestamp after which requests are allowed again
/// </summary>
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>
/// ctor
/// </summary>
/// <param name="after"></param>
public RetryAfterGuard(DateTime after)
/// <param name="type"></param>
public RetryAfterGuard(DateTime after, RateLimitItemType type)
{
After = after;
Type = type;
}
/// <inheritdoc />
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;
if (dif <= TimeSpan.Zero)
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
/// </summary>
/// <param name="retryAfter">The time after which requests can be send again</param>
/// <param name="type">RateLimitType</param>
/// <returns></returns>
Task SetRetryAfterGuardAsync(DateTime retryAfter);
Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request);
/// <summary>
/// Returns the 'retry after' timestamp if set

View File

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

View File

@ -116,6 +116,9 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc />
public event Func<int, Task>? OnRequestRateLimited;
/// <inheritdoc />
public event Func<Task>? OnConnectRateLimited;
/// <inheritdoc />
public event Func<Exception, Task>? OnError;
@ -186,6 +189,9 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true;
#endif
}
catch (PlatformNotSupportedException)
{
@ -220,6 +226,26 @@ namespace CryptoExchange.Net.Sockets
// if _ctsSource was canceled this was already logged
_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());
}

View File

@ -71,6 +71,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action<IMessageAccessor>? UnhandledMessage;
/// <summary>
/// Connection was rate limited and couldn't be established
/// </summary>
public Func<Task>? ConnectRateLimitedAsync;
/// <summary>
/// The amount of subscriptions on this connection
/// </summary>
@ -222,6 +227,7 @@ namespace CryptoExchange.Net.Sockets
_socket.OnStreamMessage += HandleStreamMessage;
_socket.OnRequestSent += HandleRequestSentAsync;
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
_socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
_socket.OnOpen += HandleOpenAsync;
_socket.OnClose += HandleCloseAsync;
_socket.OnReconnecting += HandleReconnectingAsync;
@ -385,6 +391,16 @@ namespace CryptoExchange.Net.Sockets
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>
/// Handler for whenever a request is sent over the websocket
/// </summary>

View File

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