diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 2838af4..6f1abfa 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -176,12 +176,12 @@ namespace CryptoExchange.Net.UnitTests for (var i = 0; i < requests + 1; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(i == requests? triggered : !triggered); } triggered = false; await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(!triggered); } @@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; Assert.That(expected); } @@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests RateLimitEvent evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimiting ? evnt != null : evnt == null); } @@ -243,12 +243,12 @@ namespace CryptoExchange.Net.UnitTests for (var i = 0; i < requests + 1; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(i == requests ? triggered : !triggered); } triggered = false; await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(!triggered); } @@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; Assert.That(expected); } @@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; for (var i = 0; i < 2; i++) { - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; Assert.That(expected); } @@ -309,9 +309,9 @@ namespace CryptoExchange.Net.UnitTests RateLimitEvent evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -328,9 +328,9 @@ namespace CryptoExchange.Net.UnitTests RateLimitEvent evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests RateLimitEvent evnt = null; rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -365,9 +365,9 @@ namespace CryptoExchange.Net.UnitTests 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", 1, RateLimitingBehaviour.Wait, default); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(evnt == null); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default); Assert.That(expectLimited ? evnt != null : evnt == null); } @@ -381,8 +381,8 @@ namespace CryptoExchange.Net.UnitTests rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); - var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token); - var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token); + var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); + var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); Assert.That(result2.Error, Is.TypeOf()); } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 5125138..cdc4f56 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -155,6 +155,7 @@ namespace CryptoExchange.Net.Clients /// Additional headers for this request /// Override the request weight for this request definition, for example when the weight depends on the parameters /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// protected virtual Task> SendAsync( string baseAddress, @@ -163,7 +164,8 @@ namespace CryptoExchange.Net.Clients CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null, - int? weightSingleLimiter = null) + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) { var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; return SendAsync( @@ -174,7 +176,8 @@ namespace CryptoExchange.Net.Clients cancellationToken, additionalHeaders, weight, - weightSingleLimiter); + weightSingleLimiter, + rateLimitKeySuffix); } /// @@ -189,6 +192,7 @@ namespace CryptoExchange.Net.Clients /// Additional headers for this request /// Override the request weight for this request definition, for example when the weight depends on the parameters /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// protected virtual async Task> SendAsync( string baseAddress, @@ -198,7 +202,8 @@ namespace CryptoExchange.Net.Clients CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null, - int? weightSingleLimiter = null) + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) { string? cacheKey = null; if (ShouldCache(definition)) @@ -222,7 +227,7 @@ namespace CryptoExchange.Net.Clients currentTry++; var requestId = ExchangeHelpers.NextId(); - var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter).ConfigureAwait(false); + var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false); if (!prepareResult) return new WebCallResult(prepareResult.Error!); @@ -264,6 +269,7 @@ namespace CryptoExchange.Net.Clients /// Additional headers for this request /// Override the request weight for this request /// Specify the weight to apply to the individual rate limit guard for this request + /// An additional optional suffix for the key selector /// /// protected virtual async Task PrepareAsync( @@ -273,7 +279,8 @@ namespace CryptoExchange.Net.Clients CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null, - int? weightSingleLimiter = null) + int? weightSingleLimiter = null, + string? rateLimitKeySuffix = null) { // Time sync if (definition.Authenticated) @@ -308,7 +315,7 @@ namespace CryptoExchange.Net.Clients if (ClientOptions.RateLimiterEnabled) { - var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); + var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); if (!limitResult) return new CallResult(limitResult.Error!); } @@ -323,7 +330,7 @@ namespace CryptoExchange.Net.Clients if (ClientOptions.RateLimiterEnabled) { var singleRequestWeight = weightSingleLimiter ?? 1; - var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); + var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); if (!limitResult) return new CallResult(limitResult.Error!); } @@ -609,7 +616,7 @@ namespace CryptoExchange.Net.Clients if (ClientOptions.RateLimiterEnabled) { - var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); + var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, null, cancellationToken).ConfigureAwait(false); if (!limitResult) return new CallResult(limitResult.Error!); } diff --git a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs index a5d173d..6d5b971 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RateLimitGuard.cs @@ -90,7 +90,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { foreach(var filter in _filters) { @@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards if (type == RateLimitItemType.Connection) requestWeight = _connectionWeight ?? requestWeight; - var key = _keySelector(definition, host, apiKey); + var key = _keySelector(definition, host, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) { tracker = CreateTracker(); @@ -116,7 +116,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { foreach (var filter in _filters) { @@ -127,7 +127,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards if (type == RateLimitItemType.Connection) requestWeight = _connectionWeight ?? requestWeight; - var key = _keySelector(definition, host, apiKey); + var key = _keySelector(definition, host, apiKey) + keySuffix; var tracker = _trackers[key]; tracker.ApplyWeight(requestWeight); return RateLimitState.Applied(Limit, TimeSpan, tracker.Current); diff --git a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs index 9383d54..c9c4bbe 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs @@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { if (type != Type) return LimitCheck.NotApplicable; @@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { return RateLimitState.NotApplied; } diff --git a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs index 6ad431b..f3e7998 100644 --- a/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Guards/SingleLimitGuard.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards /// /// Endpoint limit per API key /// - public static Func PerApiKey { get; } = new Func((def, host, key) => def.Path + def.Method); + public static Func PerApiKey { get; } = new Func((def, host, key) => def.Path + def.Method + key); private readonly Dictionary _trackers; private readonly RateLimitWindowType _windowType; @@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { - var key = _keySelector(definition, host, apiKey); + var key = _keySelector(definition, host, apiKey) + keySuffix; if (!_trackers.TryGetValue(key, out var tracker)) { tracker = CreateTracker(); @@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards } /// - public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) + public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix) { - var key = _keySelector(definition, host, apiKey); + var key = _keySelector(definition, host, apiKey) + keySuffix; var tracker = _trackers[key]; tracker.ApplyWeight(requestWeight); return RateLimitState.Applied(_limit, _period, tracker.Current); diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs index e4e7cf2..eb38e6a 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs @@ -53,9 +53,10 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// The API key /// Request weight /// Behaviour when rate limit is hit + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct); + Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); /// /// 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 @@ -69,8 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// The API key /// Behaviour when rate limit is hit /// The weight to apply to the limit guard + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// Cancelation token /// Error if RateLimitingBehaviour is Fail and rate limit is hit - Task ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct); + Task ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); } } diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs index 57f5e06..9a3f6a7 100644 --- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs +++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGuard.cs @@ -25,8 +25,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// The host address /// The API key /// The request weight + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); + LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); /// /// Apply the request to this guard with the specified weight @@ -36,7 +37,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces /// The host address /// The API key /// The request weight + /// An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters. /// - RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); + RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs index 0557c87..8eb089a 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs @@ -37,14 +37,14 @@ namespace CryptoExchange.Net.RateLimiting } /// - public async Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct) + public async Task ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) { await _semaphore.WaitAsync(ct).ConfigureAwait(false); bool release = true; _waitingCount++; try { - return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); + return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } catch (TaskCanceledException) { @@ -71,6 +71,7 @@ namespace CryptoExchange.Net.RateLimiting string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, + string? keySuffix, CancellationToken ct) { await _semaphore.WaitAsync(ct).ConfigureAwait(false); @@ -78,7 +79,7 @@ namespace CryptoExchange.Net.RateLimiting _waitingCount++; try { - return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); + return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } catch (TaskCanceledException) { @@ -94,12 +95,12 @@ namespace CryptoExchange.Net.RateLimiting } } - private async Task CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct) + private async Task CheckGuardsAsync(IEnumerable guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, 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); + var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix); if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) { // Delay is needed and limit behaviour is to fail the request @@ -126,14 +127,14 @@ namespace CryptoExchange.Net.RateLimiting RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false); - return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); + return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } } // Apply the weight on each guard foreach (var guard in guards) { - var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight); + var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix); if (result.IsApplied) { RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index c0b3576..32877fd 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -219,7 +219,7 @@ namespace CryptoExchange.Net.Sockets if (Parameters.RateLimiter != null) { var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, _ctsSource.Token).ConfigureAwait(false); + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); if (!limitResult) return new CallResult(new ClientRateLimitError("Connection limit reached")); } @@ -508,7 +508,7 @@ namespace CryptoExchange.Net.Sockets { try { - var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, _ctsSource.Token).ConfigureAwait(false); + var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); if (!limitResult) { await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);