1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 08:53:01 +00:00

Compare commits

...

6 Commits

7 changed files with 130 additions and 39 deletions
@@ -370,5 +370,20 @@ namespace CryptoExchange.Net.UnitTests
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
}
}
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>7.11.0</PackageVersion>
<AssemblyVersion>7.11.0</AssemblyVersion>
<FileVersion>7.11.0</FileVersion>
<PackageVersion>7.11.2</PackageVersion>
<AssemblyVersion>7.11.2</AssemblyVersion>
<FileVersion>7.11.2</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -25,6 +25,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
@@ -33,6 +34,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
static CryptoExchangeWebSocketClientLoggingExtension()
{
@@ -170,6 +172,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
LogLevel.Debug,
new EventId(1028, "ReceivedCloseMessage"),
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(1028, "SocketProcessingStateChanged"),
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
}
public static void SocketConnecting(
@@ -286,6 +299,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedCloseConfirmation(
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
{
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes)
{
@@ -333,5 +352,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
}
public static void SocketProcessingStateChanged(
this ILogger logger, int socketId, string prevState, string newState)
{
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
}
}
}
@@ -32,22 +32,30 @@ namespace CryptoExchange.Net.RateLimiting
{
_name = name;
_guards = new ConcurrentBag<IRateLimitGuard>();
_semaphore = new SemaphoreSlim(1);
_semaphore = new SemaphoreSlim(1, 1);
}
/// <inheritdoc />
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++;
try
{
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally
{
_waitingCount--;
_semaphore.Release();
if (release)
_semaphore.Release();
}
}
@@ -64,16 +72,23 @@ namespace CryptoExchange.Net.RateLimiting
CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally
{
_waitingCount--;
_semaphore.Release();
if (release)
_semaphore.Release();
}
}
@@ -233,14 +233,14 @@ namespace CryptoExchange.Net.Sockets
while (!_stopRequested)
{
_logger.SocketStartingProcessing(Id);
_processState = ProcessState.Processing;
SetProcessState(ProcessState.Processing);
var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync();
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_logger.SocketFinishedProcessing(Id);
_processState = ProcessState.WaitingForClose;
SetProcessState(ProcessState.WaitingForClose);
while (_closeTask == null)
await Task.Delay(50).ConfigureAwait(false);
@@ -250,14 +250,14 @@ namespace CryptoExchange.Net.Sockets
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
{
_processState = ProcessState.Idle;
SetProcessState(ProcessState.Idle);
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return;
}
if (!_stopRequested)
{
_processState = ProcessState.Reconnecting;
SetProcessState(ProcessState.Reconnecting);
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}
@@ -296,12 +296,15 @@ namespace CryptoExchange.Net.Sockets
_reconnectAttempt = 0;
_lastReconnectTime = DateTime.UtcNow;
// Set to processing before reconnect handling
SetProcessState(ProcessState.Processing);
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break;
}
}
_processState = ProcessState.Idle;
SetProcessState(ProcessState.Idle);
}
private TimeSpan GetReconnectDelay()
@@ -391,34 +394,33 @@ namespace CryptoExchange.Net.Sockets
if (_disposed)
return;
_ctsSource.Cancel();
if (_socket.State == WebSocketState.Open)
try
{
try
{
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
}
catch (Exception)
{
// Can sometimes throw an exception when socket is in aborted state due to timing
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
// So socket might go to aborted state, might still be open
}
}
else if(_socket.State == WebSocketState.CloseReceived)
{
try
if (_socket.State == WebSocketState.CloseReceived)
{
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
}
catch (Exception)
else if (_socket.State == WebSocketState.Open)
{
// Can sometimes throw an exception when socket is in aborted state due to timing
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
// So socket might go to aborted state, might still be open
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
var startWait = DateTime.UtcNow;
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
{
// Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
break; // Wait for max 5 seconds, then just abort the connection
}
}
}
catch (Exception)
{
// Can sometimes throw an exception when socket is in aborted state due to timing
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
// So socket might go to aborted state, might still be open
}
_ctsSource.Cancel();
}
/// <summary>
@@ -565,10 +567,20 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
// Connection closed unexpectedly
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
// Connection closed
if (_socket.State == WebSocketState.CloseReceived)
{
// Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
}
else
{
// Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
}
break;
}
@@ -758,6 +770,15 @@ namespace CryptoExchange.Net.Sockets
if (proxy.Login != null)
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
}
private void SetProcessState(ProcessState state)
{
if (_processState == state)
return;
_logger.SocketProcessingStateChanged(Id, _processState.ToString(), state.ToString());
_processState = state;
}
}
/// <summary>
@@ -151,13 +151,22 @@ namespace CryptoExchange.Net.Testing.Comparers
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
{
var resultProperties = obj.GetType().GetProperties(
var publicProperties = obj.GetType().GetProperties(
System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.GetProperty
| System.Reflection.BindingFlags.SetProperty
| System.Reflection.BindingFlags.Instance).Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
var internalProperties = obj.GetType().GetProperties(
System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.GetProperty
| System.Reflection.BindingFlags.SetProperty
| System.Reflection.BindingFlags.Instance)
.Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(JsonIncludeAttribute)))
.Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
var resultProperties = publicProperties.Concat(internalProperties);
// Property has a value
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p;
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
+6
View File
@@ -47,6 +47,12 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
* Version 7.11.2 - 28 Aug 2024
* Fixed issues when ratelimiting is canceled using the provided cancellation token
* Version 7.11.1 - 25 Aug 2024
* Improved closing logic websockets
* Version 7.11.0 - 07 Aug 2024
* Added ParseString static method on EnumConverter for parsing strings manually
* Added support for decimal values in System.Text.Json NumberStringConverter