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

Compare commits

..

15 Commits

Author SHA1 Message Date
JKorf bb7ba5ea49 Updated version 2024-04-23 19:03:34 +02:00
JKorf 747c986644 Update DateTimeConverter.cs 2024-04-23 16:44:42 +02:00
JKorf d88087c8ac Fix concurrency issue request definition cache 2024-04-23 14:56:10 +02:00
JKorf 968bdc330e Added new datetime format support 2024-04-23 14:55:57 +02:00
JKorf 7b49562c1d Fixed reconnect url logging even when url hasn't changed 2024-04-23 09:11:05 +02:00
JKorf 24ba60da47 Added error message context 2024-04-21 11:48:11 +02:00
JKorf ed5a07fbdb Updated version 2024-04-19 11:47:21 +02:00
JKorf 3d3a9b88e7 Fix for endpoint specific rate limiting throwing exception 2024-04-19 11:45:46 +02:00
JKorf b2f9d5753e Updated version 2024-04-18 19:25:53 +02:00
JKorf de46c7bd1d Don't mark system subscriptions as unconfirmed when reconnecting 2024-04-18 13:39:00 +02:00
JKorf 5b11d94f73 Docs 2024-04-17 13:57:00 +02:00
JKorf 6f915a3739 Updated version 2024-04-17 13:39:52 +02:00
JKorf d5c4b1bd01 Rename RatelimiterEnabled option to RateLimiterEnabled 2024-04-17 13:37:45 +02:00
Jan Korf 1b1961db00 Feature/ratelimit refactor (#197) 2024-04-16 14:55:27 +02:00
Jonnern 2dbd5be924 Get the state of ApiClient, SocketConnection, and Subscription as a record (#195) 2024-04-16 14:37:00 +02:00
63 changed files with 2594 additions and 767 deletions
+130 -82
View File
@@ -13,6 +13,11 @@ using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using System.Net;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.UnitTests
{
@@ -107,14 +112,14 @@ namespace CryptoExchange.Net.UnitTests
// arrange
// act
var options = new TestClientOptions();
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
options.Api1Options.OutputOriginalData = true;
options.RequestTimeout = TimeSpan.FromMinutes(1);
var client = new TestBaseClient(options);
// assert
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
@@ -162,18 +167,22 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)]
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
var triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(i == requests? result1.Data > 1 : result1.Data == 0);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result2.Data == 0);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered);
}
[TestCase("/sapi/test1", true)]
@@ -183,29 +192,40 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
[TestCase("/sapi/", "/sapi/", true)]
[TestCase("/sapi/test", "/sapi/test", true)]
[TestCase("/sapi/test", "/sapi/test123", false)]
[TestCase("/sapi/test", "/sapi/", false)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimiting ? result2.Data > 0 : result2.Data == 0);
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
[TestCase(1, 0.1)]
@@ -214,18 +234,22 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)]
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
bool triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(i == requests ? result1.Data > 1 : result1.Data == 0);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result2.Data == 0);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered);
}
[TestCase("/", false)]
@@ -233,13 +257,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test/123", false)]
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
@@ -250,47 +278,41 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test23", false)]
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("/sapi/test", "/sapi/test", true)]
@@ -298,29 +320,55 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/", "/sapi/test2", true)]
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("/sapi/test", true, true, true, false)]
[TestCase("/sapi/test", false, true, true, false)]
[TestCase("/sapi/test", false, true, false, true)]
[TestCase("/sapi/test", true, true, false, true)]
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.That(result1.Data == 0);
Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0);
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[TestCase("https://test.com", "https://test.com", true)]
[TestCase("https://test2.com", "https://test.com", false)]
[TestCase("https://test.com", "https://test2.com", false)]
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
}
}
@@ -139,12 +139,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
}
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
}
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
#pragma warning disable 0067
public event Func<Task> OnReconnected;
public event Func<Task> OnReconnecting;
public event Func<int, Task> OnRequestRateLimited;
#pragma warning restore 0067
public event Func<int, Task> OnRequestSent;
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
@@ -62,13 +63,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
}
public Task<bool> ConnectAsync()
public Task<CallResult> ConnectAsync()
{
Connected = CanConnect;
ConnectCalls++;
if (CanConnect)
InvokeOpen();
return Task.FromResult(CanConnect);
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
}
public void Send(int requestId, string data, int weight)
@@ -92,7 +92,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub).Result;
}
@@ -1,13 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
@@ -108,5 +108,19 @@ namespace CryptoExchange.Net.Clients
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
}
}
}
+318 -37
View File
@@ -13,6 +13,8 @@ using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging;
@@ -55,11 +57,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// List of rate limiters
/// </summary>
internal IEnumerable<IRateLimiter> RateLimiters { get; }
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
@@ -94,11 +91,6 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
}
@@ -114,6 +106,255 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult> SendAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
return result.AsDataless();
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
{
int currentTry = 0;
while (true)
{
currentTry++;
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!);
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
continue;
return result;
}
}
/// <summary>
/// Prepare before sending a request. Sync time between client and server and check rate limits
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var requestId = ExchangeHelpers.NextId();
var requestWeight = weight ?? definition.Weight;
// Time sync
if (definition.Authenticated)
{
if (AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new CallResult<IRequest>(new NoApiCredentialsError());
}
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
return syncTimeResult.AsDataless();
}
}
}
// Rate limiting
if (requestWeight != 0)
{
if (definition.RateLimitGate == null)
throw new Exception("Ratelimit gate not set when request weight is not 0");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
}
// Endpoint specific rate limiting
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
{
if (definition.RateLimitGate == null)
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
}
return new CallResult(null);
}
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest CreateRequest(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
Dictionary<string, string>? additionalHeaders)
{
parameters ??= new ParameterCollection();
var uri = new Uri(baseAddress.AppendPath(definition.Path));
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId();
for (var i = 0; i < parameters.Count; i++)
{
var kvp = parameters.ElementAt(i);
if (kvp.Value is Func<object> delegateValue)
parameters[kvp.Key] = delegateValue();
}
if (parameterPosition == HttpMethodParameterPosition.InUri)
{
foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
}
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
if (AuthenticationProvider != null)
{
try
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
definition.Method,
parameters,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat,
out uriParameters,
out bodyParameters,
out headers);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
}
// Sanity check
foreach (var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
{
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
uri = uri.SetParameters(uriParameters, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if (additionalHeaders != null)
{
foreach (var header in additionalHeaders)
request.AddHeader(header.Key, header.Value);
}
if (StandardRequestHeaders != null)
{
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
}
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any())
WriteParamBody(request, bodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
return request;
}
/// <summary>
/// Execute a request to the uri and returns if it was successful
/// </summary>
@@ -127,7 +368,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <param name="gate">The ratelimit gate to use</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult> SendRequestAsync(
@@ -141,23 +382,23 @@ namespace CryptoExchange.Net.Clients
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
IRateLimitGate? gate = null)
{
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync<object>(request.Data, gate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
return result.AsDataless();
@@ -178,7 +419,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <param name="gate">The ratelimit gate to use</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
@@ -192,24 +433,24 @@ namespace CryptoExchange.Net.Clients
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false
IRateLimitGate? gate = null
) where T : class
{
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
var result = await GetResponseAsync<T>(request.Data, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync<T>(request.Data, gate, cancellationToken).ConfigureAwait(false);
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
return result;
@@ -229,7 +470,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
/// <param name="gate">The rate limit gate to use</param>
/// <returns></returns>
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
Uri uri,
@@ -242,12 +483,18 @@ namespace CryptoExchange.Net.Clients
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
IRateLimitGate? gate = null)
{
var requestId = ExchangeHelpers.NextId();
if (signed)
{
if (AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
return new CallResult<IRequest>(new NoApiCredentialsError());
}
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
@@ -262,23 +509,20 @@ namespace CryptoExchange.Net.Clients
}
}
}
if (!ignoreRatelimit)
if (requestWeight != 0)
{
foreach (var limiter in RateLimiters)
if (gate == null)
throw new Exception("Ratelimit gate not set when request weight is not 0");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult<IRequest>(limitResult.Error!);
}
}
if (signed && AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
return new CallResult<IRequest>(new NoApiCredentialsError());
}
_logger.RestApiCreatingRequest(requestId, uri);
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders);
@@ -300,10 +544,12 @@ namespace CryptoExchange.Net.Clients
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
/// <param name="request">The request object to execute</param>
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
IRequest request,
IRateLimitGate? gate,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
@@ -328,9 +574,20 @@ namespace CryptoExchange.Net.Clients
Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
{
var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
error = rateError;
}
else
{
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
}
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
@@ -346,7 +603,7 @@ namespace CryptoExchange.Net.Clients
if (!valid)
{
// Invalid json
var error = new ServerError(accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
var error = new ServerError("Failed to parse response", accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
}
@@ -400,10 +657,34 @@ namespace CryptoExchange.Net.Clients
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <param name="gate">The rate limit gate the call used</param>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
protected virtual async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
{
if (tries >= 2)
// Only retry once
return false;
if ((int?)callResult.ResponseStatusCode == 429
&& ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null)
{
var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false);
if (retryTime == null)
return false;
if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60))
{
_logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value);
return true;
}
}
return false;
}
/// <summary>
/// Creates a request object
@@ -559,7 +840,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <returns></returns>
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
{
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
+86 -43
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using System;
@@ -59,7 +60,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// The rate limiters
/// </summary>
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
protected internal IRateLimitGate? RateLimiter { get; set; }
/// <summary>
/// The max size a websocket message size can be
@@ -67,7 +68,7 @@ namespace CryptoExchange.Net.Clients
protected internal int? MessageSendSizeLimit { get; set; }
/// <summary>
/// Periodic task regisrations
/// Periodic task registrations
/// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
@@ -121,10 +122,6 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
}
/// <summary>
@@ -344,20 +341,20 @@ namespace CryptoExchange.Net.Clients
/// <param name="socket">The connection to check</param>
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <returns></returns>
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
{
if (socket.Connected)
return new CallResult<bool>(true);
return new CallResult(null);
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
if (!connectResult)
return new CallResult<bool>(connectResult.Error!);
return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true);
return new CallResult(null);
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
}
@@ -367,10 +364,10 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="socket">Socket to authenticate</param>
/// <returns></returns>
public virtual async Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socket)
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
{
if (AuthenticationProvider == null)
return new CallResult<bool>(new NoApiCredentialsError());
return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest();
@@ -385,13 +382,13 @@ namespace CryptoExchange.Net.Clients
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(result.Error)!;
return new CallResult(result.Error)!;
}
}
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true;
return new CallResult<bool>(true);
return new CallResult(null);
}
/// <summary>
@@ -499,16 +496,17 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="socketConnection">The socket to connect</param>
/// <returns></returns>
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection)
{
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
var connectResult = await socketConnection.ConnectAsync().ConfigureAwait(false);
if (connectResult)
{
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
return new CallResult<bool>(true);
return connectResult;
}
socketConnection.Dispose();
return new CallResult<bool>(new CantConnectError());
return connectResult;
}
/// <summary>
@@ -521,7 +519,8 @@ namespace CryptoExchange.Net.Clients
{
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiters = RateLimiters,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehaviour = ClientOptions.RateLimitingBehaviour,
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
};
@@ -622,32 +621,76 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public string GetSubscriptionsState(bool includeSubDetails = true)
{
var sb = new StringBuilder();
sb.AppendLine($"{GetType().Name}");
sb.AppendLine($" Connections: {socketConnections.Count}");
sb.AppendLine($" Subscriptions: {CurrentSubscriptions}");
sb.AppendLine($" Download speed: {IncomingKbps} kbps");
foreach (var connection in socketConnections)
return GetState(includeSubDetails).ToString();
}
/// <summary>
/// Gets the state of the client
/// </summary>
/// <param name="includeSubDetails">True to get details for each subscription</param>
/// <returns></returns>
public SocketApiClientState GetState(bool includeSubDetails = true)
{
var connectionStates = new List<SocketConnection.SocketConnectionState>();
foreach (var socketIdAndConnection in socketConnections)
{
sb.AppendLine($" Id: {connection.Key}");
sb.AppendLine($" Address: {connection.Value.ConnectionUri}");
sb.AppendLine($" Subscriptions: {connection.Value.UserSubscriptionCount}");
sb.AppendLine($" Status: {connection.Value.Status}");
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
sb.AppendLine($" Subscriptions:");
if (includeSubDetails)
{
foreach (var subscription in connection.Value.Subscriptions)
{
sb.AppendLine($" Id: {subscription.Id}");
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
}
}
SocketConnection connection = socketIdAndConnection.Value;
SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
connectionStates.Add(connectionState);
}
return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
}
/// <summary>
/// Get the current state of the client
/// </summary>
/// <param name="Connections">Number of sockets for this client</param>
/// <param name="Subscriptions">Total number of subscriptions</param>
/// <param name="DownloadSpeed">Total download speed</param>
/// <param name="ConnectionStates">State of each socket connection</param>
public record SocketApiClientState(
int Connections,
int Subscriptions,
double DownloadSpeed,
List<SocketConnection.SocketConnectionState> ConnectionStates)
{
/// <summary>
/// Print the state of the client
/// </summary>
/// <param name="sb"></param>
/// <returns></returns>
protected virtual bool PrintMembers(StringBuilder sb)
{
sb.AppendLine();
sb.AppendLine($"\tTotal connections: {Connections}");
sb.AppendLine($"\tTotal subscriptions: {Subscriptions}");
sb.AppendLine($"\tDownload speed: {DownloadSpeed} kbps");
sb.AppendLine($"\tConnections:");
ConnectionStates.ForEach(cs =>
{
sb.AppendLine($"\t\tId: {cs.Id}");
sb.AppendLine($"\t\tAddress: {cs.Address}");
sb.AppendLine($"\t\tTotal subscriptions: {cs.Subscriptions}");
sb.AppendLine($"\t\tStatus: {cs.Status}");
sb.AppendLine($"\t\tAuthenticated: {cs.Authenticated}");
sb.AppendLine($"\t\tDownload speed: {cs.DownloadSpeed} kbps");
sb.AppendLine($"\t\tPending queries: {cs.PendingQueries}");
if (cs.SubscriptionStates?.Count > 0)
{
sb.AppendLine($"\t\tSubscriptions:");
cs.SubscriptionStates.ForEach(subState =>
{
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]");
});
}
});
return true;
}
return sb.ToString();
}
/// <summary>
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters
{
@@ -68,6 +68,21 @@ namespace CryptoExchange.Net.Converters.JsonNet
return objectType == typeof(DateTime) ? default(DateTime) : null;
}
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
@@ -68,7 +68,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
}
if (stringValue!.Length == 8)
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
+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.2.1</PackageVersion>
<AssemblyVersion>7.2.1</AssemblyVersion>
<FileVersion>7.2.1</FileVersion>
<PackageVersion>7.3.3</PackageVersion>
<AssemblyVersion>7.3.3</AssemblyVersion>
<FileVersion>7.3.3</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>
-6
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Linq;
@@ -9,12 +8,7 @@ using System.Security;
using System.Text;
using System.Web;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Collections;
using System.Net.Http;
using System.Data.Common;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
@@ -1,8 +1,6 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Interfaces
{
@@ -1,8 +1,4 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
using System;
namespace CryptoExchange.Net.Interfaces
{
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
+7 -3
View File
@@ -1,5 +1,5 @@
using System;
using System.IO;
using CryptoExchange.Net.Objects;
using System;
using System.Net.WebSockets;
using System.Threading.Tasks;
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Func<int, Task> OnRequestSent;
/// <summary>
/// Websocket query was ratelimited and couldn't be send
/// </summary>
event Func<int, Task>? OnRequestRateLimited;
/// <summary>
/// Websocket error event
/// </summary>
event Func<Exception, Task> OnError;
@@ -67,7 +71,7 @@ namespace CryptoExchange.Net.Interfaces
/// Connect the socket
/// </summary>
/// <returns></returns>
Task<bool> ConnectAsync();
Task<CallResult> ConnectAsync();
/// <summary>
/// Send data
/// </summary>
@@ -20,7 +20,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _closed;
private static readonly Action<ILogger, int, Exception?> _disposing;
private static readonly Action<ILogger, int, Exception?> _disposed;
private static readonly Action<ILogger, int, int, int, Exception?> _sendDelayedBecauseOfRateLimit;
private static readonly Action<ILogger, int, int, int, Exception?> _sentBytes;
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
@@ -74,7 +73,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_addingBytesToSendBuffer = LoggerMessage.Define<int, int, int>(
LogLevel.Trace,
new EventId(1007, "AddingBytesToSendBuffer"),
"[Sckt {SocketId}] msg {RequestId} - Adding {NumBytes} bytes to send buffer");
"[Sckt {SocketId}] [Req {RequestId}] adding {NumBytes} bytes to send buffer");
_reconnectRequested = LoggerMessage.Define<int>(
LogLevel.Debug,
@@ -111,15 +110,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(1014, "Disposed"),
"[Sckt {SocketId}] disposed");
_sendDelayedBecauseOfRateLimit = LoggerMessage.Define<int, int, int>(
LogLevel.Debug,
new EventId(1015, "SendDelayedBecauseOfRateLimit"),
"[Sckt {SocketId}] msg {RequestId} - send delayed {DelayMS}ms because of rate limit");
_sentBytes = LoggerMessage.Define<int, int, int>(
LogLevel.Trace,
new EventId(1016, "SentBytes"),
"[Sckt {SocketId}] msg {RequestId} - sent {NumBytes} bytes");
"[Sckt {SocketId}] [Req {RequestId}] sent {NumBytes} bytes");
_sendLoopStoppedWithException = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -267,12 +261,6 @@ namespace CryptoExchange.Net.Logging.Extensions
_disposed(logger, socketId, null);
}
public static void SocketSendDelayedBecauseOfRateLimit(
this ILogger logger, int socketId, int requestId, int delay)
{
_sendDelayedBecauseOfRateLimit(logger, socketId, requestId, delay, null);
}
public static void SocketSentBytes(
this ILogger logger, int socketId, int requestId, int numBytes)
{
@@ -0,0 +1,78 @@
using Microsoft.Extensions.Logging;
using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class RateLimitGateLoggingExtensions
{
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
private static readonly Action<ILogger, int, string, TimeSpan, string, string, Exception?> _rateLimitDelayingRequest;
private static readonly Action<ILogger, int, TimeSpan, string, string, Exception?> _rateLimitDelayingConnection;
private static readonly Action<ILogger, int, string, string, string, int, Exception?> _rateLimitAppliedRequest;
private static readonly Action<ILogger, int, string, string, int, Exception?> _rateLimitAppliedConnection;
static RateLimitGateLoggingExtensions()
{
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(6000, "RateLimitRequestFailed"),
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(6001, "RateLimitConnectionFailed"),
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6002, "RateLimitDelayingRequest"),
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6003, "RateLimitDelayingConnection"),
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
LogLevel.Trace,
new EventId(6004, "RateLimitDelayingConnection"),
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
LogLevel.Trace,
new EventId(6005, "RateLimitAppliedRequest"),
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
}
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
{
_rateLimitRequestFailed(logger, requestId, path, guard, limit, null);
}
public static void RateLimitConnectionFailed(this ILogger logger, int connectionId, string guard, string limit)
{
_rateLimitConnectionFailed(logger, connectionId, guard, limit, null);
}
public static void RateLimitDelayingRequest(this ILogger logger, int requestId, string path, TimeSpan delay, string guard, string limit)
{
_rateLimitDelayingRequest(logger, requestId, path, delay, guard, limit, null);
}
public static void RateLimitDelayingConnection(this ILogger logger, int connectionId, TimeSpan delay, string guard, string limit)
{
_rateLimitDelayingConnection(logger, connectionId, delay, guard, limit, null);
}
public static void RateLimitAppliedConnection(this ILogger logger, int connectionId, string guard, string limit, int current)
{
_rateLimitAppliedConnection(logger, connectionId, guard, limit, current, null);
}
public static void RateLimitAppliedRequest(this ILogger logger, int requestIdId, string path, string guard, string limit, int current)
{
_rateLimitAppliedRequest(logger, requestIdId, path, guard, limit, current, null);
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Net.Http;
@@ -13,6 +14,9 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
private static readonly Action<ILogger, int, Uri, Exception?> _restApiCreatingRequest;
private static readonly Action<ILogger, int, HttpMethod, string, Uri, string, Exception?> _restApiSendingRequest;
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
static RestApiClientLoggingExtensions()
@@ -46,6 +50,21 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Trace,
new EventId(4005, "RestApiSendingRequest"),
"[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}");
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4006, "RestApiRateLimitRetry"),
"[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}");
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4007, "RestApiRateLimitPauseUntil"),
"[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}");
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
LogLevel.Debug,
new EventId(4008, "RestApiSendRequest"),
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
@@ -77,5 +96,20 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiSendingRequest(logger, requestId, method, signed, uri, paramString, null);
}
public static void RestApiRateLimitRetry(this ILogger logger, int requestId, DateTime retryAfter)
{
_restApiRateLimitRetry(logger, requestId, retryAfter, null);
}
public static void RestApiRateLimitPauseUntil(this ILogger logger, int requestId, DateTime retryAfter)
{
_restApiRateLimitPauseUntil(logger, requestId, retryAfter, null);
}
public static void RestApiSendRequest(this ILogger logger, int requestId, RequestDefinition definition, string? body, string query, string headers)
{
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
}
}
}
@@ -1,6 +1,5 @@
using System;
using System.Net.WebSockets;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
@@ -73,7 +72,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_messageSentNotPending = LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(2006, "MessageSentNotPending"),
"[Sckt {SocketId}] msg {RequestId} - message sent, but not pending");
"[Sckt {SocketId}] [Req {RequestId}] message sent, but not pending");
_receivedData = LoggerMessage.Define<int, string>(
LogLevel.Trace,
@@ -178,12 +177,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_periodicSendFailed = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(2027, "PeriodicSendFailed"),
"[Sckt {SocketId}] Periodic send {Identifier} failed: {ErrorMessage}");
"[Sckt {SocketId}] periodic send {Identifier} failed: {ErrorMessage}");
_sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] msg {RequestId} - sending messsage: {Data}");
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
+23
View File
@@ -15,6 +15,29 @@
Wait
}
/// <summary>
/// What to do when a request would exceed the rate limit
/// </summary>
public enum RateLimitWindowType
{
/// <summary>
/// A sliding window
/// </summary>
Sliding,
/// <summary>
/// A fixed interval window
/// </summary>
Fixed,
/// <summary>
/// A fixed interval starting after the first request
/// </summary>
FixedAfterFirst,
/// <summary>
/// Decaying window
/// </summary>
Decay
}
/// <summary>
/// Where the parameters for a HttpMethod should be added in a request
/// </summary>
@@ -28,6 +28,15 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Whether or not client side rate limiting should be applied
/// </summary>
public bool RateLimiterEnabled { get; set; } = true;
/// <summary>
/// What should happen when a rate limit is reached
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
/// <inheritdoc />
public override string ToString()
{
@@ -1,7 +1,5 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects.Options
{
@@ -10,16 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class RestApiOptions : ApiOptions
{
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <summary>
/// What to do when a call would exceed the rate limit
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
@@ -42,8 +30,6 @@ namespace CryptoExchange.Net.Objects.Options
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
RateLimiters = RateLimiters,
RateLimitingBehaviour = RateLimitingBehaviour,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
}
@@ -32,7 +32,9 @@ namespace CryptoExchange.Net.Objects.Options
TimestampRecalculationInterval = TimestampRecalculationInterval,
ApiCredentials = ApiCredentials?.Copy(),
Proxy = Proxy,
RequestTimeout = RequestTimeout
RequestTimeout = RequestTimeout,
RateLimiterEnabled = RateLimiterEnabled,
RateLimitingBehaviour = RateLimitingBehaviour
};
}
}
@@ -1,7 +1,5 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects.Options
{
@@ -10,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class SocketApiOptions : ApiOptions
{
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <summary>
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
/// for example when the server sends intermittent ping requests
@@ -37,7 +30,6 @@ namespace CryptoExchange.Net.Objects.Options
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
RateLimiters = RateLimiters,
SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections,
};
@@ -65,7 +65,9 @@ namespace CryptoExchange.Net.Objects.Options
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
MaxSocketConnections = MaxSocketConnections,
Proxy = Proxy,
RequestTimeout = RequestTimeout
RequestTimeout = RequestTimeout,
RateLimitingBehaviour = RateLimitingBehaviour,
RateLimiterEnabled = RateLimiterEnabled,
};
}
}
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
-443
View File
@@ -1,443 +0,0 @@
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Limits the amount of requests to a certain constraint
/// </summary>
public class RateLimiter : IRateLimiter
{
private readonly object _limiterLock = new object();
internal List<Limiter> _limiters = new List<Limiter>();
/// <summary>
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
/// <see cref="AddEndpointLimit(string, int, TimeSpan, HttpMethod?, bool)"/>, <see cref="AddPartialEndpointLimit(string, int, TimeSpan, HttpMethod?, bool, bool)"/> or <see cref="AddApiKeyLimit"/>.
/// </summary>
public RateLimiter()
{
}
/// <summary>
/// Add a rate limit for the total amount of requests per time period
/// </summary>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{
lock(_limiterLock)
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoint">The endpoint the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoints">The endpoints the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{
lock(_limiterLock)
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this;
}
/// <summary>
/// Add a rate lmit for the amount of requests per time for an endpoint
/// </summary>
/// <param name="endpoint">The endpoint the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="method">The HttpMethod the limit is for, null for all</param>
/// <param name="ignoreOtherRateLimits">If set to true it ignores other rate limits</param>
/// <param name="countPerEndpoint">Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit</param>
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
{
lock(_limiterLock)
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
return this;
}
/// <summary>
/// Add a rate limit for the amount of requests per Api key
/// </summary>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
/// <param name="onlyForSignedRequests">Only include calls that are signed in this limiter</param>
/// <param name="excludeFromTotalRateLimit">Exclude requests with API key from the total rate limiter</param>
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{
lock(_limiterLock)
_limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
return this;
}
/// <summary>
/// Add a rate limit for the amount of messages that can be send per connection
/// </summary>
/// <param name="endpoint">The endpoint that the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
{
lock (_limiterLock)
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
return this;
}
/// <inheritdoc />
public async Task<CallResult<int>> LimitRequestAsync(ILogger logger, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
{
int totalWaitTime = 0;
List<EndpointRateLimiter> endpointLimits;
lock (_limiterLock)
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
foreach (var endpointLimit in endpointLimits)
{
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime);
List<PartialEndpointRateLimiter> partialEndpointLimits;
lock (_limiterLock)
partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
foreach (var partialEndpointLimit in partialEndpointLimits)
{
if (partialEndpointLimit.CountPerEndpoint)
{
SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock)
{
thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
if (thisEndpointLimit == null)
{
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
_limiters.Add(thisEndpointLimit);
}
}
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
else
{
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime);
List<ApiKeyRateLimiter> apiLimits;
lock (_limiterLock)
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
foreach (var apiLimit in apiLimits)
{
if(apiKey == null)
{
if (!apiLimit.OnlyForSignedRequests)
{
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
else if (signed || !apiLimit.OnlyForSignedRequests)
{
SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock)
{
thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
if (thisApiLimit == null)
{
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
_limiters.Add(thisApiLimit);
}
}
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
}
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
return new CallResult<int>(totalWaitTime);
List<TotalRateLimiter> totalLimits;
lock (_limiterLock)
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
foreach(var totalLimit in totalLimits)
{
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
if (!waitResult)
return waitResult;
totalWaitTime += waitResult.Data;
}
return new CallResult<int>(totalWaitTime);
}
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
{
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult<int>(new CancellationRequestedError());
}
sw.Stop();
try
{
int totalWaitTime = 0;
while (true)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
for (var i = 0; i < historyTopic.Entries.Count; i++)
{
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
{
historyTopic.Entries.Remove(historyTopic.Entries[i]);
i--;
}
else
break;
}
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
if (currentWeight + requestWeight > historyTopic.Limit)
{
if (currentWeight == 0)
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
// Wait until the next entry should be removed from the history
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
if (thisWaitTime > 0)
{
if (limitBehaviour == RateLimitingBehaviour.Fail)
{
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
logger.Log(LogLevel.Warning, msg);
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
}
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
try
{
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return new CallResult<int>(new CancellationRequestedError());
}
totalWaitTime += thisWaitTime;
}
}
else
{
break;
}
}
var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
return new CallResult<int>(totalWaitTime);
}
finally
{
historyTopic.Semaphore.Release();
}
}
internal struct LimitEntry
{
public DateTime Timestamp { get; set; }
public int Weight { get; set; }
public LimitEntry(DateTime timestamp, int weight)
{
Timestamp = timestamp;
Weight = weight;
}
}
internal class Limiter
{
public RateLimitType Type { get; set; }
public HttpMethod? Method { get; set; }
public SemaphoreSlim Semaphore { get; set; }
public int Limit { get; set; }
public TimeSpan Period { get; set; }
public List<LimitEntry> Entries { get; set; } = new List<LimitEntry>();
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
{
Semaphore = new SemaphoreSlim(1, 1);
Type = type;
Limit = limit;
Period = perPeriod;
Method = method;
}
}
internal class TotalRateLimiter : Limiter
{
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
: base(RateLimitType.Total, limit, perPeriod, method)
{
}
public override string ToString()
{
return nameof(TotalRateLimiter);
}
}
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
{
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
: base(new[] { "/" }, limit, perPeriod, null, true, true)
{
}
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
: base(endpoints, limit, perPeriod, null, true, true)
{
}
public override string ToString()
{
return nameof(ConnectionRateLimiter);
}
}
internal class EndpointRateLimiter: Limiter
{
public string[] Endpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
:base(RateLimitType.Endpoint, limit, perPeriod, method)
{
Endpoints = endpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
}
public override string ToString()
{
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
}
}
internal class PartialEndpointRateLimiter : Limiter
{
public string[] PartialEndpoints { get; set; }
public bool IgnoreOtherRateLimits { get; set; }
public bool CountPerEndpoint { get; set; }
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
{
PartialEndpoints = partialEndpoints;
IgnoreOtherRateLimits = ignoreOtherRateLimits;
CountPerEndpoint = countPerEndpoint;
}
public override string ToString()
{
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
}
}
internal class ApiKeyRateLimiter : Limiter
{
public bool OnlyForSignedRequests { get; set; }
public bool IgnoreTotalRateLimit { get; set; }
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
:base(RateLimitType.ApiKey, limit, perPeriod, method)
{
OnlyForSignedRequests = onlyForSignedRequests;
IgnoreTotalRateLimit = ignoreTotalRateLimit;
}
}
internal class SingleTopicRateLimiter: Limiter
{
public object Topic { get; set; }
public SingleTopicRateLimiter(object topic, Limiter limiter)
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
{
Topic = topic;
}
public override string ToString()
{
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
}
}
internal enum RateLimitType
{
Total,
Endpoint,
PartialEndpoint,
ApiKey
}
}
}
@@ -0,0 +1,81 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// The definition of a rest request
/// </summary>
public class RequestDefinition
{
private string? _stringRep;
// Basics
/// <summary>
/// Path of the request
/// </summary>
public string Path { get; set; }
/// <summary>
/// Http method of the request
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Is the request authenticated
/// </summary>
public bool Authenticated { get; set; }
// Formating
/// <summary>
/// The body format for this request
/// </summary>
public RequestBodyFormat? RequestBodyFormat { get; set; }
/// <summary>
/// The position of parameters for this request
/// </summary>
public HttpMethodParameterPosition? ParameterPosition { get; set; }
/// <summary>
/// The array serialization type for this request
/// </summary>
public ArrayParametersSerialization? ArraySerialization { get; set; }
// Rate limiting
/// <summary>
/// Request weight
/// </summary>
public int Weight { get; set; } = 1;
/// <summary>
/// Rate limit gate to use
/// </summary>
public IRateLimitGate? RateLimitGate { get; set; }
/// <summary>
/// Rate limit for this specific endpoint
/// </summary>
public int? EndpointLimitCount { get; set; }
/// <summary>
/// Rate limit period for this specific endpoint
/// </summary>
public TimeSpan? EndpointLimitPeriod { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
/// <param name="method"></param>
public RequestDefinition(string path, HttpMethod method)
{
Path = path;
Method = method;
}
/// <inheritdoc />
public override string ToString()
{
return _stringRep ??= $"{Method} {Path}{(Authenticated ? " authenticated" : "")}";
}
}
}
@@ -0,0 +1,84 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Request definitions cache
/// </summary>
public class RequestDefinitionCache
{
private readonly ConcurrentDictionary<string, RequestDefinition> _definitions = new();
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false)
=> GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
=> GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
int? endpointLimitCount = null,
TimeSpan? endpointLimitPeriod = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null)
{
if (!_definitions.TryGetValue(method + path, out var def))
{
def = new RequestDefinition(path, method)
{
Authenticated = authenticated,
EndpointLimitCount = endpointLimitCount,
EndpointLimitPeriod = endpointLimitPeriod,
RateLimitGate = rateLimitGate,
Weight = weight,
ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition,
};
_definitions.TryAdd(method + path, def);
}
return def;
}
}
}
@@ -1,4 +1,4 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
@@ -51,9 +51,13 @@ namespace CryptoExchange.Net.Objects.Sockets
public TimeSpan? KeepAliveInterval { get; set; }
/// <summary>
/// The rate limiters for the socket connection
/// The rate limiter for the socket connection
/// </summary>
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
public IRateLimitGate? RateLimiter { get; set; }
/// <summary>
/// What to do when rate limit is reached
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; }
/// <summary>
/// Encoding for sending/receiving data
@@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether they're authenticated or not
/// </summary>
public class AuthenticatedEndpointFilter : IGuardFilter
{
private readonly bool _authenticated;
/// <summary>
/// ctor
/// </summary>
/// <param name="authenticated"></param>
public AuthenticatedEndpointFilter(bool authenticated)
{
_authenticated = authenticated;
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> definition.Authenticated == _authenticated;
}
}
@@ -0,0 +1,30 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether the request path matches a specific path
/// </summary>
public class ExactPathFilter : IGuardFilter
{
private readonly string _path;
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
public ExactPathFilter(string path)
{
_path = path;
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,28 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether the request path matches any specific path in a list
/// </summary>
public class ExactPathsFilter : IGuardFilter
{
private readonly HashSet<string> _paths;
/// <summary>
/// ctor
/// </summary>
/// <param name="paths"></param>
public ExactPathsFilter(IEnumerable<string> paths)
{
_paths = new HashSet<string>(paths);
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> _paths.Contains(definition.Path);
}
}
@@ -0,0 +1,28 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether the host address matches a specific address
/// </summary>
public class HostFilter : IGuardFilter
{
private readonly string _host;
/// <summary>
/// ctor
/// </summary>
/// <param name="host"></param>
public HostFilter(string host)
{
_host = host;
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> host == _host;
}
}
@@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether it's a connection or a request
/// </summary>
public class LimitItemTypeFilter : IGuardFilter
{
private readonly RateLimitItemType _type;
/// <summary>
/// ctor
/// </summary>
/// <param name="type"></param>
public LimitItemTypeFilter(RateLimitItemType type)
{
_type = type;
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> type == _type;
}
}
@@ -0,0 +1,28 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
/// <summary>
/// Filter requests based on whether the path starts with a specific string
/// </summary>
public class PathStartFilter : IGuardFilter
{
private readonly string _path;
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
public PathStartFilter(string path)
{
_path = path;
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,146 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards
{
/// <inheritdoc />
public class RateLimitGuard : IRateLimitGuard
{
/// <summary>
/// Apply guard per host
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
/// <summary>
/// Apply guard per endpoint
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per API key
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
/// <summary>
/// Apply guard per API key per endpoint
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
private readonly IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers;
private RateLimitWindowType _windowType;
private double? _decayRate;
private int? _connectionWeight;
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
/// <inheritdoc />
public string Name => "RateLimitGuard";
/// <inheritdoc />
public string Description => _windowType == RateLimitWindowType.Decay ? $"Limit of {Limit} with a decay rate of {_decayRate}" : $"Limit of {Limit} per {TimeSpan}";
/// <summary>
/// The limit per period
/// </summary>
public int Limit { get; }
/// <summary>
/// The time period for the limit
/// </summary>
public TimeSpan TimeSpan { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="keySelector">The rate limit key selector</param>
/// <param name="filter">Filter for rate limit items. Only when the rate limit item passes the filter the guard will apply</param>
/// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="keySelector">The rate limit key selector</param>
/// <param name="filters">Filters for rate limit items. Only when the rate limit item passes all filters the guard will apply</param>
/// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
{
_filters = filters;
_trackers = new Dictionary<string, IWindowTracker>();
_windowType = windowType;
Limit = limit;
TimeSpan = timeSpan;
_keySelector = keySelector;
_decayRate = decayPerTimeSpan;
_connectionWeight = connectionWeight;
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
foreach(var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
return LimitCheck.NotApplicable;
}
if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey);
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker();
_trackers.Add(key, tracker);
}
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
return RateLimitState.NotApplied;
}
if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey);
var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
}
/// <summary>
/// Create a new WindowTracker
/// </summary>
/// <returns></returns>
protected IWindowTracker CreateTracker()
{
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan)
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) :
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
}
}
}
@@ -0,0 +1,62 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards
{
/// <summary>
/// Retry after guard
/// </summary>
public class RetryAfterGuard : IRateLimitGuard
{
/// <summary>
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static readonly TimeSpan _windowBuffer = TimeSpan.FromMilliseconds(1000);
/// <inheritdoc />
public string Name => "RetryAfterGuard";
/// <inheritdoc />
public string Description => $"Pause requests until after {After}";
/// <summary>
/// The timestamp after which requests are allowed again
/// </summary>
public DateTime After { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="after"></param>
public RetryAfterGuard(DateTime after)
{
After = after;
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
var dif = (After + _windowBuffer) - DateTime.UtcNow;
if (dif <= TimeSpan.Zero)
return LimitCheck.NotApplicable;
return LimitCheck.Needed(dif, default, default, default);
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
return RateLimitState.NotApplied;
}
/// <summary>
/// Update the 'after' time
/// </summary>
/// <param name="after"></param>
public void UpdateAfter(DateTime after) => After = after;
}
}
@@ -0,0 +1,72 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using System;
using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Guards
{
/// <summary>
/// Rate limit guard for a per endpoint limit
/// </summary>
public class SingleLimitGuard : IRateLimitGuard
{
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
/// <inheritdoc />
public string Name => "EndpointLimitGuard";
/// <inheritdoc />
public string Description => $"Limit requests to endpoint";
/// <summary>
/// ctor
/// </summary>
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
{
_windowType = windowType;
_decayRate = decayRate;
_trackers = new Dictionary<string, IWindowTracker>();
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
var key = definition.Path + definition.Method;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
_trackers.Add(key, tracker);
}
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{
var key = definition.Path + definition.Method;
var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
}
/// <summary>
/// Create a new WindowTracker
/// </summary>
/// <returns></returns>
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
{
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
}
}
}
@@ -0,0 +1,21 @@
using CryptoExchange.Net.Objects;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
/// <summary>
/// Filter requests based on specific condition
/// </summary>
public interface IGuardFilter
{
/// <summary>
/// Whether a request or connection passes this filter
/// </summary>
/// <param name="type">The type of item</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns>
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
}
}
@@ -0,0 +1,78 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Guards;
using Microsoft.Extensions.Logging;
using System;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
/// <summary>
/// Rate limit gate
/// </summary>
public interface IRateLimitGate
{
/// <summary>
/// Event when the rate limit is triggered
/// </summary>
event Action<RateLimitEvent> RateLimitTriggered;
/// <summary>
/// Add a rate limit guard
/// </summary>
/// <param name="guard">Guard to add</param>
/// <returns></returns>
IRateLimitGate AddGuard(IRateLimitGuard guard);
/// <summary>
/// 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>
/// <returns></returns>
Task SetRetryAfterGuardAsync(DateTime retryAfter);
/// <summary>
/// Set the SingleLimitGuard for handling individual endpoint rate limits
/// </summary>
/// <param name="guard"></param>
/// <returns></returns>
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
/// <summary>
/// Returns the 'retry after' timestamp if set
/// </summary>
/// <returns></returns>
Task<DateTime?> GetRetryAfterTime();
/// <summary>
/// Process a request. Enforces the configured rate limits. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="itemId">Id of the item to check</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
/// <summary>
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="itemId">Id of the item to check</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
}
}
@@ -0,0 +1,44 @@
using CryptoExchange.Net.Objects;
using System.Net.Http;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
/// <summary>
/// Rate limit guard
/// </summary>
public interface IRateLimitGuard
{
/// <summary>
/// Name
/// </summary>
string Name { get; }
/// <summary>
/// Description
/// </summary>
string Description { get; }
/// <summary>
/// Check whether a request can pass this rate limit guard
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
/// <summary>
/// Apply the request to this guard with the specified weight
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
}
}
@@ -0,0 +1,34 @@
using System;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
/// <summary>
/// Rate limit window tracker
/// </summary>
public interface IWindowTracker
{
/// <summary>
/// Time period the limit is for
/// </summary>
TimeSpan TimePeriod { get; }
/// <summary>
/// The limit in the time period
/// </summary>
int Limit { get; }
/// <summary>
/// The current count within the time period
/// </summary>
int Current { get; }
/// <summary>
/// Get the time to wait to fit the weight
/// </summary>
/// <param name="weight"></param>
/// <returns></returns>
TimeSpan GetWaitTime(int weight);
/// <summary>
/// Register the weight in this window
/// </summary>
/// <param name="weight">Request weight</param>
void ApplyWeight(int weight);
}
}
@@ -0,0 +1,60 @@
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Limit check
/// </summary>
public readonly struct LimitCheck
{
/// <summary>
/// Is guard applicable
/// </summary>
public bool Applicable { get; }
/// <summary>
/// Delay needed
/// </summary>
public TimeSpan Delay { get; }
/// <summary>
/// Current counter
/// </summary>
public int Current { get; }
/// <summary>
/// Limit
/// </summary>
public int? Limit { get; }
/// <summary>
/// Time period
/// </summary>
public TimeSpan? Period { get; }
private LimitCheck(bool applicable, TimeSpan delay, int limit, TimeSpan period, int current)
{
Applicable = applicable;
Delay = delay;
Limit = limit;
Period = period;
Current = current;
}
/// <summary>
/// Not applicable
/// </summary>
public static LimitCheck NotApplicable { get; } = new LimitCheck(false, default, default, default, default);
/// <summary>
/// No wait needed
/// </summary>
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
/// <summary>
/// Wait needed
/// </summary>
/// <param name="delay">The delay needed</param>
/// <param name="limit">Limit per period</param>
/// <param name="period">Period the limit is for</param>
/// <param name="current">Current counter</param>
/// <returns></returns>
public static LimitCheck Needed(TimeSpan delay, int limit, TimeSpan period, int current) => new(true, delay, limit, period, current);
}
}
@@ -0,0 +1,30 @@
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// A rate limit entry
/// </summary>
public struct LimitEntry
{
/// <summary>
/// Timestamp of the item
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// Item weight
/// </summary>
public int Weight { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="timestamp"></param>
/// <param name="weight"></param>
public LimitEntry(DateTime timestamp, int weight)
{
Timestamp = timestamp;
Weight = weight;
}
}
}
@@ -0,0 +1,80 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit event
/// </summary>
public record RateLimitEvent
{
/// <summary>
/// Name of the API limit that is reached
/// </summary>
public string ApiLimit { get; set; } = string.Empty;
/// <summary>
/// Description of the limit that is reached
/// </summary>
public string LimitDescription { get; set; } = string.Empty;
/// <summary>
/// The request definition
/// </summary>
public RequestDefinition RequestDefinition { get; set; }
/// <summary>
/// The host the request is for
/// </summary>
public string Host { get; set; } = default!;
/// <summary>
/// The current counter value
/// </summary>
public int Current { get; set; }
/// <summary>
/// The weight of the limited request
/// </summary>
public int RequestWeight { get; set; }
/// <summary>
/// The limit per time period
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// The time period the limit is for
/// </summary>
public TimeSpan? TimePeriod { get; set; }
/// <summary>
/// The time the request will be delayed for if the Behaviour is RateLimitingBehaviour.Wait
/// </summary>
public TimeSpan? DelayTime { get; set; }
/// <summary>
/// The handling behaviour for the rquest
/// </summary>
public RateLimitingBehaviour Behaviour { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="apiLimit"></param>
/// <param name="limitDescription"></param>
/// <param name="definition"></param>
/// <param name="host"></param>
/// <param name="current"></param>
/// <param name="requestWeight"></param>
/// <param name="limit"></param>
/// <param name="timePeriod"></param>
/// <param name="delayTime"></param>
/// <param name="behaviour"></param>
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{
ApiLimit = apiLimit;
LimitDescription = limitDescription;
RequestDefinition = definition;
Host = host;
Current = current;
RequestWeight = requestWeight;
Limit = limit;
TimePeriod = timePeriod;
DelayTime = delayTime;
Behaviour = behaviour;
}
}
}
@@ -0,0 +1,174 @@
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.RateLimiting
{
/// <inheritdoc />
public class RateLimitGate : IRateLimitGate
{
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
private readonly ConcurrentBag<IRateLimitGuard> _guards;
private readonly SemaphoreSlim _semaphore;
private readonly string _name;
private int _waitingCount;
/// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered;
/// <summary>
/// ctor
/// </summary>
public RateLimitGate(string name)
{
_name = name;
_guards = new ConcurrentBag<IRateLimitGuard>();
_semaphore = new SemaphoreSlim(1);
}
/// <inheritdoc />
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
_waitingCount++;
try
{
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
finally
{
_waitingCount--;
_semaphore.Release();
}
}
/// <inheritdoc />
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
if (requestWeight == 0)
requestWeight = 1;
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
finally
{
_waitingCount--;
_semaphore.Release();
}
}
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
foreach (var guard in guards)
{
// Check if a wait is needed for this guard
var result = guard.Check(type, definition, host, apiKey, requestWeight);
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
{
// Delay is needed and limit behaviour is to fail the request
if (type == RateLimitItemType.Connection)
logger.RateLimitConnectionFailed(itemId, guard.Name, guard.Description);
else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
}
if (result.Delay != TimeSpan.Zero)
{
// 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}";
if (type == RateLimitItemType.Connection)
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
}
// Apply the weight on each guard
foreach (var guard in guards)
{
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied)
{
if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else
logger.RateLimitAppliedRequest(itemId, definition.Path, guard.Name, guard.Description, result.Current);
}
}
return new CallResult(null);
}
/// <inheritdoc />
public IRateLimitGate AddGuard(IRateLimitGuard guard)
{
_guards.Add(guard);
return this;
}
/// <inheritdoc />
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
{
_singleLimitGuard = guard;
return this;
}
/// <inheritdoc />
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
{
await _semaphore.WaitAsync().ConfigureAwait(false);
try
{
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
if (retryAfterGuard == null)
_guards.Add(new RetryAfterGuard(retryAfter));
else
retryAfterGuard.UpdateAfter(retryAfter);
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc />
public async Task<DateTime?> GetRetryAfterTime()
{
await _semaphore.WaitAsync().ConfigureAwait(false);
try
{
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
return retryAfterGuard?.After;
}
finally
{
_semaphore.Release();
}
}
}
}
@@ -0,0 +1,20 @@
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit item type
/// </summary>
[Flags]
public enum RateLimitItemType
{
/// <summary>
/// A connection attempt
/// </summary>
Connection = 1,
/// <summary>
/// A request
/// </summary>
Request = 2
}
}
@@ -0,0 +1,55 @@
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Limit state
/// </summary>
public struct RateLimitState
{
/// <summary>
/// Limit
/// </summary>
public int Limit { get; }
/// <summary>
/// Period
/// </summary>
public TimeSpan Period { get; }
/// <summary>
/// Current count
/// </summary>
public int Current { get; }
/// <summary>
/// Whether the limit is applied
/// </summary>
public bool IsApplied { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="applied"></param>
/// <param name="limit"></param>
/// <param name="period"></param>
/// <param name="current"></param>
public RateLimitState(bool applied, int limit, TimeSpan period, int current)
{
IsApplied = applied;
Limit = limit;
Period = period;
Current = current;
}
/// <summary>
/// Not applied result
/// </summary>
public static RateLimitState NotApplied { get; } = new RateLimitState(false, default, default, default);
/// <summary>
/// Applied result
/// </summary>
/// <param name="limit"></param>
/// <param name="period"></param>
/// <param name="current"></param>
/// <returns></returns>
public static RateLimitState Applied(int limit, TimeSpan period, int current) => new RateLimitState(true, limit, period, current);
}
}
@@ -0,0 +1,86 @@
using System;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class DecayWindowTracker : IWindowTracker
{
/// <inheritdoc />
public TimeSpan TimePeriod { get; }
/// <summary>
/// Decrease rate per TimePeriod
/// </summary>
public double DecreaseRate { get; }
/// <inheritdoc />
public int Limit { get; }
/// <inheritdoc />
public int Current => _currentWeight;
private int _currentWeight = 0;
private DateTime _lastDecrease = DateTime.UtcNow;
public DecayWindowTracker(int limit, TimeSpan period, double decayRate)
{
Limit = limit;
TimePeriod = period;
DecreaseRate = decayRate;
}
/// <inheritdoc />
public TimeSpan GetWaitTime(int weight)
{
// Decrease the counter based on the last update time and decay rate
DecreaseCounter(DateTime.UtcNow);
if (Current + weight > Limit)
{
// The weight would cause the rate limit to be passed
if (Current == 0)
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}");
}
// Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime(weight);
}
// Weight can fit without going over limit
return TimeSpan.Zero;
}
/// <inheritdoc />
public void ApplyWeight(int weight)
{
if (_currentWeight == 0)
_lastDecrease = DateTime.UtcNow;
_currentWeight += weight;
}
/// <summary>
/// Decrease the counter based on time passed since last update and the decay rate
/// </summary>
/// <param name="time"></param>
protected void DecreaseCounter(DateTime time)
{
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate;
var decrease = (int)Math.Floor(dif);
if (decrease >= 1)
{
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif));
_lastDecrease = time;
}
}
/// <summary>
/// Determine the time to wait before the weight would fit
/// </summary>
/// <param name="requestWeight"></param>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight)
{
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
}
}
}
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class FixedAfterStartWindowTracker : IWindowTracker
{
/// <inheritdoc />
public TimeSpan TimePeriod { get; }
/// <inheritdoc />
public int Limit { get; }
/// <inheritdoc />
public int Current => _currentWeight;
private readonly Queue<LimitEntry> _entries;
private int _currentWeight = 0;
private DateTime? _nextReset;
/// <summary>
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedAfterStartWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
TimePeriod = period;
_entries = new Queue<LimitEntry>();
}
public TimeSpan GetWaitTime(int weight)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
if (_nextReset != null && checkTime > _nextReset)
RemoveBefore(_nextReset.Value);
if (Current == 0)
_nextReset = null;
if (Current + weight > Limit)
{
// The weight would cause the rate limit to be passed
if (Current == 0)
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}");
}
// Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime();
}
// Weight can fit without going over limit
return TimeSpan.Zero;
}
/// <inheritdoc />
public void ApplyWeight(int weight)
{
if (_currentWeight == 0)
_nextReset = DateTime.UtcNow + TimePeriod;
_currentWeight += weight;
_entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight));
}
/// <summary>
/// Remove items before a certain time
/// </summary>
/// <param name="time"></param>
protected void RemoveBefore(DateTime time)
{
while (true)
{
if (_entries.Count == 0)
break;
var firstItem = _entries.Peek();
if (firstItem.Timestamp < time)
{
_entries.Dequeue();
_currentWeight -= firstItem.Weight;
}
else
{
// Either no entries left, or the entry time is still within the window
break;
}
}
}
/// <summary>
/// Determine the time to wait before a new item would fit
/// </summary>
/// <returns></returns>
private TimeSpan DetermineWaitTime()
{
var checkTime = DateTime.UtcNow;
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
}
}
}
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class FixedWindowTracker : IWindowTracker
{
/// <inheritdoc />
public TimeSpan TimePeriod { get; }
/// <inheritdoc />
public int Limit { get; }
/// <inheritdoc />
public int Current => _currentWeight;
private readonly Queue<LimitEntry> _entries;
private int _currentWeight = 0;
/// <summary>
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
TimePeriod = period;
_entries = new Queue<LimitEntry>();
}
/// <inheritdoc />
public TimeSpan GetWaitTime(int weight)
{
// Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow;
RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)));
if (Current + weight > Limit)
{
// The weight would cause the rate limit to be passed
if (Current == 0)
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}");
}
// Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime();
}
// Weight can fit without going over limit
return TimeSpan.Zero;
}
/// <inheritdoc />
public void ApplyWeight(int weight)
{
_currentWeight += weight;
_entries.Enqueue(new LimitEntry(DateTime.UtcNow, weight));
}
/// <summary>
/// Remove items before a certain time
/// </summary>
/// <param name="time"></param>
protected void RemoveBefore(DateTime time)
{
while (true)
{
if (_entries.Count == 0)
break;
var firstItem = _entries.Peek();
if (firstItem.Timestamp < time)
{
_entries.Dequeue();
_currentWeight -= firstItem.Weight;
}
else
{
// Either no entries left, or the entry time is still within the window
break;
}
}
}
/// <summary>
/// Determine the time to wait before a new item would fit
/// </summary>
/// <returns></returns>
private TimeSpan DetermineWaitTime()
{
var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
return wait.Add(_fixedWindowBuffer);
}
}
}
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using CryptoExchange.Net.RateLimiting.Interfaces;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal class SlidingWindowTracker : IWindowTracker
{
/// <inheritdoc />
public TimeSpan TimePeriod { get; }
/// <inheritdoc />
public int Limit { get; }
/// <inheritdoc />
public int Current => _currentWeight;
private readonly List<LimitEntry> _entries;
private int _currentWeight = 0;
public SlidingWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
TimePeriod = period;
_entries = new List<LimitEntry>();
}
/// <inheritdoc />
public TimeSpan GetWaitTime(int weight)
{
// Remove requests no longer in time period from the history
RemoveBefore(DateTime.UtcNow - TimePeriod);
if (Current + weight > Limit)
{
// The weight would cause the rate limit to be passed
if (Current == 0)
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}");
}
// Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime(weight);
}
// Weight can fit without going over limit
return TimeSpan.Zero;
}
/// <inheritdoc />
public void ApplyWeight(int weight)
{
_currentWeight += weight;
_entries.Add(new LimitEntry(DateTime.UtcNow, weight));
}
/// <summary>
/// Remove items before a certain time
/// </summary>
/// <param name="time"></param>
protected void RemoveBefore(DateTime time)
{
for (var i = 0; i < _entries.Count; i++)
{
if (_entries[i].Timestamp < time)
{
var entry = _entries[i];
_entries.Remove(entry);
_currentWeight -= entry.Weight;
i--;
}
else
{
break;
}
}
}
/// <summary>
/// Determine the time to wait before the weight would fit
/// </summary>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight)
{
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
var removedWeight = 0;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
removedWeight += entry.Weight;
if (removedWeight >= weightToRemove)
{
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
}
}
throw new Exception("Request not possible to execute with current rate limit guard. " +
$" Request weight: {requestWeight}, Ratelimit: {Limit}");
}
}
}
@@ -2,6 +2,7 @@
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -45,6 +46,7 @@ namespace CryptoExchange.Net.Sockets
private bool _disposed;
private ProcessState _processState;
private DateTime _lastReconnectTime;
private string _baseAddress;
private const int _receiveBufferSize = 1048576;
private const int _sendBufferSize = 4096;
@@ -110,6 +112,9 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc />
public event Func<int, Task>? OnRequestSent;
/// <inheritdoc />
public event Func<int, Task>? OnRequestRateLimited;
/// <inheritdoc />
public event Func<Exception, Task>? OnError;
@@ -143,17 +148,19 @@ namespace CryptoExchange.Net.Sockets
_closeSem = new SemaphoreSlim(1, 1);
_socket = CreateSocket();
_baseAddress = $"{Uri.Scheme}://{Uri.Host}";
}
/// <inheritdoc />
public virtual async Task<bool> ConnectAsync()
public virtual async Task<CallResult> ConnectAsync()
{
if (!await ConnectInternalAsync().ConfigureAwait(false))
return false;
var connectResult = await ConnectInternalAsync().ConfigureAwait(false);
if (!connectResult)
return connectResult;
await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_processTask = ProcessAsync();
return true;
return connectResult;
}
/// <summary>
@@ -188,11 +195,19 @@ namespace CryptoExchange.Net.Sockets
return socket;
}
private async Task<bool> ConnectInternalAsync()
private async Task<CallResult> ConnectInternalAsync()
{
_logger.SocketConnecting(Id);
try
{
if (Parameters.RateLimiter != null)
{
var definition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult)
return new CallResult(new ClientRateLimitError("Connection limit reached"));
}
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(tcs.Token, _ctsSource.Token);
await _socket.ConnectAsync(Uri, linked.Token).ConfigureAwait(false);
@@ -204,11 +219,11 @@ namespace CryptoExchange.Net.Sockets
// if _ctsSource was canceled this was already logged
_logger.SocketConnectionFailed(Id, e.Message, e);
}
return false;
return new CallResult(new CantConnectError());
}
_logger.SocketConnected(Id, Uri);
return true;
return new CallResult(null);
}
/// <inheritdoc />
@@ -255,7 +270,7 @@ namespace CryptoExchange.Net.Sockets
if (task != null)
{
var reconnectUri = await task.ConfigureAwait(false);
if (reconnectUri != null && Parameters.Uri != reconnectUri)
if (reconnectUri != null && Parameters.Uri.ToString() != reconnectUri.ToString())
{
_logger.SocketSetReconnectUri(Id, reconnectUri);
Parameters.Uri = reconnectUri;
@@ -407,9 +422,9 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
private async Task SendLoopAsync()
{
var requestDefinition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
try
{
var limitKey = Uri.ToString() + "/" + Id.ToString();
while (true)
{
if (_ctsSource.IsCancellationRequested)
@@ -422,16 +437,13 @@ namespace CryptoExchange.Net.Sockets
while (_sendBuffer.TryDequeue(out var data))
{
if (Parameters.RateLimiters != null)
if (Parameters.RateLimiter != null)
{
foreach(var ratelimiter in Parameters.RateLimiters)
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult)
{
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.SocketSendDelayedBecauseOfRateLimit(Id, data.Id, limitResult.Data);
}
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
continue;
}
}
@@ -1,7 +1,5 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Sockets
{
-1
View File
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+71 -10
View File
@@ -19,6 +19,28 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public class SocketConnection
{
/// <summary>
/// State of a the connection
/// </summary>
/// <param name="Id">The id of the socket connection</param>
/// <param name="Address">The connection URI</param>
/// <param name="Subscriptions">Number of subscriptions on this socket</param>
/// <param name="Status">Socket status</param>
/// <param name="Authenticated">If the connection is authenticated</param>
/// <param name="DownloadSpeed">Download speed over this socket</param>
/// <param name="PendingQueries">Number of non-completed queries</param>
/// <param name="SubscriptionStates">State for each subscription on this socket</param>
public record SocketConnectionState(
int Id,
string Address,
int Subscriptions,
SocketStatus Status,
bool Authenticated,
double DownloadSpeed,
int PendingQueries,
List<Subscription.SubscriptionState> SubscriptionStates
);
/// <summary>
/// Connection lost event
/// </summary>
@@ -194,6 +216,7 @@ namespace CryptoExchange.Net.Sockets
_socket = socket;
_socket.OnStreamMessage += HandleStreamMessage;
_socket.OnRequestSent += HandleRequestSentAsync;
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
_socket.OnOpen += HandleOpenAsync;
_socket.OnClose += HandleCloseAsync;
_socket.OnReconnecting += HandleReconnectingAsync;
@@ -228,7 +251,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
foreach (var subscription in _listeners.OfType<Subscription>())
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
subscription.Confirmed = false;
foreach (var query in _listeners.OfType<Query>().ToList())
@@ -253,7 +276,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
foreach (var subscription in _listeners.OfType<Subscription>())
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
subscription.Confirmed = false;
foreach (var query in _listeners.OfType<Query>().ToList())
@@ -337,6 +360,26 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask;
}
/// <summary>
/// Handler for whenever a request is rate limited and rate limit behaviour is set to fail
/// </summary>
/// <param name="requestId"></param>
/// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{
Query query;
lock (_listenersLock)
{
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
}
if (query == null)
return Task.CompletedTask;
query.Fail(new ClientRateLimitError("Connection rate limit reached"));
return Task.CompletedTask;
}
/// <summary>
/// Handler for whenever a request is sent over the websocket
/// </summary>
@@ -478,7 +521,7 @@ namespace CryptoExchange.Net.Sockets
/// Connect the websocket
/// </summary>
/// <returns></returns>
public async Task<bool> ConnectAsync() => await _socket.ConnectAsync().ConfigureAwait(false);
public async Task<CallResult> ConnectAsync() => await _socket.ConnectAsync().ConfigureAwait(false);
/// <summary>
/// Retrieve the underlying socket
@@ -620,6 +663,24 @@ namespace CryptoExchange.Net.Sockets
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
}
/// <summary>
/// Get the state of the connection
/// </summary>
/// <returns></returns>
public SocketConnectionState GetState(bool includeSubDetails)
{
return new SocketConnectionState(
SocketId,
ConnectionUri.AbsoluteUri,
UserSubscriptionCount,
Status,
Authenticated,
IncomingKbps,
PendingQueries: _listeners.OfType<Query>().Count(x => !x.Completed),
includeSubDetails ? Subscriptions.Select(sub => sub.GetState()).ToList() : new List<Subscription.SubscriptionState>()
);
}
/// <summary>
/// Send a query request and wait for an answer
/// </summary>
@@ -710,13 +771,13 @@ namespace CryptoExchange.Net.Sockets
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] msg {RequestId} - {Info}", SocketId, requestId, info);
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
if (!_socket.IsOpen)
{
_logger.LogWarning("[Sckt {SocketId}] msg {RequestId} - Failed to send, socket no longer open", SocketId, requestId);
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId);
return new CallResult(new WebError("Failed to send message, socket no longer open"));
}
@@ -735,7 +796,7 @@ namespace CryptoExchange.Net.Sockets
private async Task<CallResult> ProcessReconnectAsync()
{
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
return new CallResult(new WebError("Socket not connected"));
bool anySubscriptions;
lock (_listenersLock)
@@ -745,7 +806,7 @@ namespace CryptoExchange.Net.Sockets
// No need to resubscribe anything
_logger.NothingToResubscribeCloseConnection(SocketId);
_ = _socket.CloseAsync();
return new CallResult<bool>(true);
return new CallResult(null);
}
bool anyAuthenticated;
@@ -785,7 +846,7 @@ namespace CryptoExchange.Net.Sockets
for (var i = 0; i < subList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
{
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
return new CallResult(new WebError("Socket not connected"));
var taskList = new List<Task<CallResult>>();
foreach (var subscription in subList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
@@ -812,10 +873,10 @@ namespace CryptoExchange.Net.Sockets
subscription.Confirmed = true;
if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected"));
return new CallResult(new WebError("Socket not connected"));
_logger.AllSubscriptionResubscribed(SocketId);
return new CallResult<bool>(true);
return new CallResult(null);
}
internal async Task UnsubscribeAsync(Subscription subscription)
+23 -1
View File
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
@@ -156,6 +155,29 @@ namespace CryptoExchange.Net.Sockets
{
Exception?.Invoke(e);
}
/// <summary>
/// State of this subscription
/// </summary>
/// <param name="Id">The id of the subscription</param>
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
/// <param name="Invocations">Number of times this subscription got a message</param>
/// <param name="Identifiers">Identifiers the subscription is listening to</param>
public record SubscriptionState(
int Id,
bool Confirmed,
int Invocations,
HashSet<string> Identifiers
);
/// <summary>
/// Get the state of this subscription
/// </summary>
/// <returns></returns>
public SubscriptionState GetState()
{
return new SubscriptionState(Id, Confirmed, TotalInvocations, ListenerIdentifiers);
}
}
/// <inheritdoc />
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
+23
View File
@@ -42,6 +42,29 @@ 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.3.3 - 23 Apr 2024
* Added support for new DateTime format parsing
* Updated some logging
* Fixed concurrency issue in rest request sending
* Version 7.3.2 - 19 Apr 2024
* Fix for endpoint specific rate limiting throwing exception
* Version 7.3.1 - 18 Apr 2024
* Fixed websocket system subscriptions getting marked as unconfirmed when reconnecting
* Version 7.3.0 - 17 Apr 2024
* Added new method for sending Rest requests which splits the static and dynamic parameters
* Refactored rate limiting implementation
* Ratelimiters now statically applied for all clients
* Added support for different rate limit window types
* Added modular configuration of rate limits
* Added rate limit check when creating websocket connections
* Added automatic handling and retry for Retry-After responses
* Added configuration for setting ratelimit for each individual endpoint
* Added event for when rate limit is triggered
* Added SocketClient GetSocketApiClientStates method
* Version 7.2.1 - 05 Apr 2024
* Improved websocket reconnect logic
* Simplified SystemTextJsonMessageAccessor value retrieval
+91 -47
View File
@@ -881,7 +881,7 @@ else
</div>
<div class="tab-pane fade" id="rest-coinex" role="tabpanel" aria-labelledby="rest-coinex-tab">
<pre><code>var client = new CoinExRestClient();
var tickersResult = await client.SpotApi.ExchangeData.GetTickersAsync();
var tickersResult = await client.SpotApiV2.ExchangeData.GetTickersAsync();
if (!tickersResult.Success)
{
// Handle error, tickersResult.Error contains more information
@@ -1115,12 +1115,12 @@ if (!subscribeResult.Success)
</div>
<div class="tab-pane fade" id="socket-coinex" role="tabpanel" aria-labelledby="socket-coinex-tab">
<pre><code>var client = new CoinExSocketClient();
var subscribeResult = await client.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", update => {
// Handle the data update, update.Data will contain the actual data
var subscribeResult = await sclient.SpotApiV2.SubscribeToTickerUpdatesAsync(new[] { "ETHUSDT" }, update => {
// Handle the data update, update.Data will contain the actual data
});
if (!subscribeResult.Success)
{
// Handle error, subscribeResult.Error contains more information on why the subscription failed
// Handle error, subscribeResult.Error contains more information on why the subscription failed
}
// Subscribing was successfull, the data will now be streamed into the data handler</code></pre>
</div>
@@ -1809,6 +1809,16 @@ var client = new OKXRestClient();</code></pre>
<td>When enabled the originally received string data will be available as well as the deserialized object. For REST API client calls the data will be in the <code>WebCallResult<T>.OriginalData</code> property, for Websocket API client subscriptions the data will be available in the <code>DataEvent<T>.OriginalData</code> property when receiving an update.</td>
<td><code>false</code></td>
</tr>
<tr>
<td>RateLimiterEnabled</td>
<td>Whether or not client side rate limiting should be applied. Note that not all libraries have ratelimiting implemented, if it's not implemented this flag does nothing</td>
<td><code>true</code></td>
</tr>
<tr>
<td>RateLimitingBehaviour</td>
<td>What should happen when a rate limit is reached. RateLimitingBehaviour.Wait: the request waits until it can be send while staying within the limits, RateLimitingBehaviour.Fail: the request will return an error</td>
<td><code>RateLimitingBehaviour.Wait</code></td>
</tr>
<tr>
<td>Environment</td>
<td>The environment the library should connect to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments</td>
@@ -1831,16 +1841,6 @@ var client = new OKXRestClient();</code></pre>
<td>The interval of how often the time synchronization between client and server should be executed</td>
<td><code>TimeSpan.FromHours(1)</code></td>
</tr>
<tr>
<td>[API].RateLimiters</td>
<td>A list of <code>IRateLimiter</code>s to use</td>
<td><code>Dependent on the library</code></td>
</tr>
<tr>
<td>[API].RateLimitingBehaviour</td>
<td>What should happen when a rate limit is reached</td>
<td><code>RateLimitingBehaviour.Wait</code></td>
</tr>
<tr>
<td>[API].ApiCredentials</td>
<td>Same as the in the base options, allows overriding per sub-API</td>
@@ -1905,11 +1905,6 @@ var client = new OKXRestClient();</code></pre>
<td>The time to wait before sending messages after connecting to the server</td>
<td><code>null</code></td>
</tr>
<tr>
<td>[API].RateLimiters</td>
<td>A list of <code>IRateLimiter</code>s to use</td>
<td><code>Dependent on the library</code></td>
</tr>
<tr>
<td>[API].SocketNoDataTimeout</td>
<td>Same as the in the base websocket client options, allows overriding per sub-API</td>
@@ -2275,32 +2270,81 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
<section id="idocs_ratelimiting">
<h2>Ratelimiting</h2>
<p>
The client libraries have build in rate limiting. These rate limits can be configured per client. Some client implementations where the exchange has clear rate limits will also have a default rate limiter already set up.
Rate limiting is configured in the client options, and can be set on a specific client or for all clients by either providing it in the constructor for a client, or by using the <code>SetDefaultOptions</code> on a client.
<div class="alert alert-info">What to do when a limit is reached can be configured with the <code>RateLimitingBehaviour</code> client options, either <code>Fail</code> or <code>Wait</code>.</div>
The client libraries have build in support for rate limiting. Rate limiting in this case means that requests are throttled (or failed before sending based on configuration) when the client detects a server rate limit will be exceeded. Whether or not rate limiting is applied can be configured in the DI registration or client options. Not all libraries currently have rate limiting configured.
<div class="alert alert-info">What to do when a limit is reached can be configured with the <code>RateLimitingBehaviour</code> client options, either <code>Fail</code> for returning an error or <code>Wait</code> to wait until the request can safely be send.</div>
</p>
<p>
<b>Ratelimit configuration</b><br />
A rate limiter can be configured in the options like so:
<pre><code>new ClientOptions
{
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
RateLimiters = new List<IRateLimiter>
{
new RateLimiter()
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
}
}</code></pre>
This will add a rate limiter for 50 requests per 10 seconds.
A rate limiter can have multiple limits:
<pre><code>new RateLimiter()
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))</code></pre>
This adds another limit of 10 requests per 2 seconds for the order endpoint in addition to the 50 requests per 10 seconds limit.
Client side rate limiting can only correctly work if there is only a single program talking to the exchange. When multiple different application send requests at the same time it's impossible for the client side to keep track of the rate limits. When using multiple concurrent applications it is advised to turn off rate limiting. Also note that when requests are rate limited with RateLimitBehaviour.Wait that the order of the requests being send is not guarenteed.
</p>
<p>Client side rate limiting is currently implemented for the following libraries:</p>
<div class="tab-wrap">
<ul class="nav nav-tabs" id="limit" role="tablist" style="margin-bottom: -16px;">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="limit-binance-tab" data-toggle="tab" href="#limit-binance" role="tab" aria-controls="limit-binance" aria-selected="true">Binance</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="limit-kraken-tab" data-toggle="tab" href="#limit-kraken" role="tab" aria-controls="limit-kraken" aria-selected="false">Kraken</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="limit-kucoin-tab" data-toggle="tab" href="#limit-kucoin" role="tab" aria-controls="limit-kucoin" aria-selected="false">Kucoin</a>
</li>
</ul>
<div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="limit-binance" role="tabpanel" aria-labelledby="limit-binance-tab">
<pre><code>services.AddBinance(x =>
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
}, x =>
{
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
});</code></pre>
<p>To be notified of when a rate limit is hit the static <code>BinanceExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
<pre><code>BinanceExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
// Output: Limit triggered: RateLimitEvent { ApiLimit = Spot Socket, LimitDescription = Limit of 6000 per 00:01:00, RequestDefinition = GET 1, Host = wss://ws-api.binance.com, Current = 5752, RequestWeight = 250, Limit = 6000, TimePeriod = 00:01:00, DelayTime = 00:00:38.7784145, Behaviour = Wait }
</code></pre>
</div>
<div class="tab-pane fade" id="limit-kraken" role="tabpanel" aria-labelledby="limit-kraken-tab">
<pre><code>services.AddKraken(x =>
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
}, x =>
{
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
});</code></pre>
<p>To be notified of when a rate limit is hit the static <code>KrakenExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
<pre><code>KrakenExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
// Output: Limit triggered: RateLimitEvent { ApiLimit = Spot Rest, LimitDescription = Limit of 15 with a decay rate of 0,33, RequestDefinition = POST 0/private/TradesHistory authenticated, Host = api.kraken.com, Current = 14, RequestWeight = 2, Limit = 15, TimePeriod = 00:00:01, DelayTime = 00:00:04, Behaviour = Wait }</code></pre>
<p>Kraken applies different rate limits based on the account verification tier. By default the rate limit is set to the most conservative <code>Starter</code> tier. To change the rate limit tier call the <code>Configure</code> method</p>
<pre><code>KrakenExchange.RateLimiter.Configure(Kraken.Net.Enums.RateLimitTier.Pro);</code></pre>
</div>
<div class="tab-pane fade" id="limit-kucoin" role="tabpanel" aria-labelledby="limit-kucoin-tab">
<pre><code>services.AddKucoin(x =>
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
}, x =>
{
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
});</code></pre>
<p>To be notified of when a rate limit is hit the static <code>KucoinExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
<pre><code>KucoinExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
// Output: Limit triggered: RateLimitEvent { ApiLimit = Public Rest, LimitDescription = Limit of 2000 per 00:00:30, RequestDefinition = GET api/v1/market/stats, Host = https://api.kucoin.com/, Current = 1995, RequestWeight = 15, Limit = 2000, TimePeriod = 00:00:30, DelayTime = 00:00:19.8111238, Behaviour = Wait }</code></pre>
<p>Kucoin applies different rate limits based on the account VIP level. By default the rate limit is set to the most conservative <code>VIP0</code> tier. To change the rate limit tier call the <code>Configure</code> method</p>
<pre><code>KucoinExchange.RateLimiter.Configure(Kucoin.Net.Enums.VipLevel.Vip5);</code></pre>
</div>
</div>
</div>
</section>
<hr class="divider">
@@ -2382,7 +2426,7 @@ await spotClient.GetSymbolsAsync();</code></pre>
<pre><code>await bybitClient.V5Api.ExchangeData.GetSpotSymbolsAsync();</code></pre>
</div>
<div class="tab-pane fade" id="example-symbols-coinex" role="tabpanel" aria-labelledby="example-symbols-coinex-tab">
<pre><code>await coinExClient.SpotApi.ExchangeData.GetSymbolsAsync();</code></pre>
<pre><code>await coinExClient.SpotApiV2.ExchangeData.GetSymbolsAsync();</code></pre>
</div>
<div class="tab-pane fade" id="example-symbols-huobi" role="tabpanel" aria-labelledby="example-symbols-huobi-tab">
<pre><code>await huobiClient.SpotApi.ExchangeData.GetSymbolsAsync();</code></pre>
@@ -2476,7 +2520,7 @@ await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code>
<pre><code>await bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");</code></pre>
</div>
<div class="tab-pane fade" id="example-ticker-coinex" role="tabpanel" aria-labelledby="example-ticker-coinex-tab">
<pre><code>await coinExClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");</code></pre>
<pre><code>await coinExClient.SpotApiV2.ExchangeData.GetTickersAsync(new[] { "BTCUSDT" });</code></pre>
</div>
<div class="tab-pane fade" id="example-ticker-huobi" role="tabpanel" aria-labelledby="example-ticker-huobi-tab">
<pre><code>await huobiClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");</code></pre>
@@ -2570,7 +2614,7 @@ await spotClient.GetBalancesAsync();</code></pre>
<pre><code>await bybitClient.V5Api.Account.GetBalancesAsync(AccountType.Spot);</code></pre>
</div>
<div class="tab-pane fade" id="example-balances-coinex" role="tabpanel" aria-labelledby="example-balances-coinex-tab">
<pre><code>await coinExClient.SpotApi.Account.GetBalancesAsync();</code></pre>
<pre><code>await coinExClient.SpotApiV2.Account.GetBalancesAsync();</code></pre>
</div>
<div class="tab-pane fade" id="example-balances-huobi" role="tabpanel" aria-labelledby="example-balances-huobi-tab">
<pre><code>// Need an account id, you probably want to already have done this before placing the order
@@ -2668,7 +2712,7 @@ await spotClient.PlaceOrderAsync(spotClient.GetSymbolName("BTC", "USDT"), Common
<pre><code>await bybitClient.V5Api.Trading.PlaceOrderAsync(Category.Spot, "BTCUSDT", OrderSide.Buy, NewOrderType.Limit, 0.1m, price: 50000);</code></pre>
</div>
<div class="tab-pane fade" id="example-place-coinex" role="tabpanel" aria-labelledby="example-place-coinex-tab">
<pre><code>await coinExClient.SpotApi.Trading.PlaceOrderAsync("BTCUSDT", OrderSide.Buy, OrderType.Limit, 0.1m, 50000);</code></pre>
<pre><code>await coinExClient.SpotApiV2.Trading.PlaceOrderAsync("BTCUSDT", AccountType.Spot, OrderSide.Buy, OrderTypeV2.Limit, 0.1m, 50000);</code></pre>
</div>
<div class="tab-pane fade" id="example-place-huobi" role="tabpanel" aria-labelledby="example-place-huobi-tab">
<pre><code>// Need an account id, you probably want to already have done this before placing the order
@@ -2768,7 +2812,7 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
});</code></pre>
</div>
<div class="tab-pane fade" id="example-stream-ticker-coinex" role="tabpanel" aria-labelledby="example-stream-ticker-coinex-tab">
<pre><code>await coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {
<pre><code>await coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(new[] { "ETHUSDT" }, data => {
// Handle update
});</code></pre>
</div>
@@ -2905,7 +2949,7 @@ _ = Task.Run(async () => {
});</code></pre>
</div>
<div class="tab-pane fade" id="example-stream-order-coinex" role="tabpanel" aria-labelledby="example-stream-order-coinex-tab">
<pre><code>await coinExSocketClient.SpotApi.SubscribeToOrderUpdatesAsync(data => {
<pre><code>await coinExSocketClient.SpotApiV2.SubscribeToOrderUpdatesAsync(data => {
// Handle update
});</code></pre>
</div>