diff --git a/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs b/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs index a89a7b49..3d26952c 100644 --- a/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs +++ b/CryptoExchange.Net.UnitTests/Implementations/TestRestApiClient.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Testing.Implementations; using Microsoft.Extensions.Logging; @@ -47,11 +48,12 @@ namespace CryptoExchange.Net.UnitTests.Implementations RequestFactory = factory; } - internal async Task> GetResponseAsync(HttpMethod? httpMethod = null, ParameterCollection? collection = null) + internal async Task> GetResponseAsync(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null) { var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get) { - Weight = 0 + Weight = 1, + RateLimitGate = rateLimitGate }; return await SendAsync(BaseAddress, definition, collection ?? new ParameterCollection(), default); } diff --git a/CryptoExchange.Net.UnitTests/RateLimitTests.cs b/CryptoExchange.Net.UnitTests/RateLimitTests.cs index 719be6b9..63a31ac3 100644 --- a/CryptoExchange.Net.UnitTests/RateLimitTests.cs +++ b/CryptoExchange.Net.UnitTests/RateLimitTests.cs @@ -3,11 +3,13 @@ using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.UnitTests.Implementations; using NUnit.Framework; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -285,5 +287,39 @@ namespace CryptoExchange.Net.UnitTests // assert Assert.That(evnt, Is.Not.Null); } + + [TestCase(null, null, true)] + [TestCase("Group1", null, false)] + [TestCase(null, "Group2", false)] + [TestCase("Group1", "Group2", false)] + [TestCase("Group3", "Group3", true)] + public async Task RateLimiterWithDifferentGroups_Should_AllowNotRateLimit(string? group1, string? group2, bool expectLimited) + { + // arrange + var data = JsonSerializer.Serialize(new TestObject { }); + var client1 = new TestRestClient(x => + { + x.RateLimitGroup = group1; + }); + client1.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK); + var client2 = new TestRestClient(x => + { + x.RateLimitGroup = group2; + }); + client2.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK); + + var rateLimiter = new RateLimitGate("Test"); + rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(2), RateLimitWindowType.Fixed)); + + RateLimitEvent? evnt = null; + rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; + + // act + var result1 = await client1.ApiClient1.GetResponseAsync(rateLimitGate: rateLimiter); + var result2 = await client2.ApiClient1.GetResponseAsync(rateLimitGate: rateLimiter); + + // assert + Assert.That(evnt != null, Is.EqualTo(expectLimited)); + } } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 498af673..6c438ded 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -342,7 +342,7 @@ namespace CryptoExchange.Net.Clients GetAuthenticationProvider()?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, - rateLimitKeySuffix, + rateLimitKeySuffix + ClientOptions.RateLimitGroup, cancellationToken).ConfigureAwait(false); if (!limitResult) return limitResult.Error!; diff --git a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs index 2851ff2c..c1907f1a 100644 --- a/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/ExchangeOptions.cs @@ -22,6 +22,10 @@ namespace CryptoExchange.Net.Objects.Options /// Note that this comes at a performance cost /// public bool OutputOriginalData { get; set; } = false; + /// + /// A group name to use for client side rate limiting. Requests with the same group name will be counted together for rate limiting purposes. If null all requests will be counted together. + /// + public string? RateLimitGroup { get; set; } /// /// The max time a request is allowed to take diff --git a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs index 2f08300c..e7869107 100644 --- a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs @@ -69,6 +69,7 @@ namespace CryptoExchange.Net.Objects.Options item.RequestTimeout = RequestTimeout; item.RateLimiterEnabled = RateLimiterEnabled; item.RateLimitingBehaviour = RateLimitingBehaviour; + item.RateLimitGroup = RateLimitGroup; item.CachingEnabled = CachingEnabled; item.CachingMaxAge = CachingMaxAge; item.HttpVersion = HttpVersion; diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs index 838d3e09..3e5f3cf7 100644 --- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs @@ -104,6 +104,7 @@ namespace CryptoExchange.Net.Objects.Options item.RequestTimeout = RequestTimeout; item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimiterEnabled = RateLimiterEnabled; + item.RateLimitGroup = RateLimitGroup; item.ReceiveBufferSize = ReceiveBufferSize; return item; }