feat(web): implement KisApiClient request throttling to prevent KIS OpenAPI rate limits
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 16s
Validators (Pushes and Pull Requests) / validate-core (push) Successful in 1m45s

This commit is contained in:
2026-07-12 14:18:44 +09:00
parent 6d9937c590
commit 82e18a9a22
@@ -32,6 +32,8 @@ public class KisApiClient : IKisApiClient
private readonly ITokenCache _tokenCache;
private readonly ILogger<KisApiClient> _logger;
private static readonly ConcurrentDictionary<string, SemaphoreSlim> TokenLocks = new();
private readonly SemaphoreSlim _rateLimitSemaphore = new(1, 1);
private DateTime _lastRequestTime = DateTime.MinValue;
public KisApiClient(HttpClient httpClient, ITokenCache tokenCache, ILogger<KisApiClient> logger)
{
@@ -152,6 +154,7 @@ public class KisApiClient : IKisApiClient
foreach (var header in headers)
request.Headers.Add(header.Key, header.Value);
await ApplyRateLimitDelayAsync(account);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
@@ -195,6 +198,7 @@ public class KisApiClient : IKisApiClient
{
try
{
await ApplyRateLimitDelayAsync(creds.Account);
var response = await _httpClient.PostAsJsonAsync(
$"{creds.Domain}/oauth2/tokenP",
tokenRequest
@@ -318,4 +322,29 @@ public class KisApiClient : IKisApiClient
return null;
}
}
private async Task ApplyRateLimitDelayAsync(string account)
{
await _rateLimitSemaphore.WaitAsync();
try
{
var mode = account.Contains("real", StringComparison.OrdinalIgnoreCase) ? "real" : "mock";
int minIntervalMs = mode == "real" ? 150 : 400;
var now = DateTime.UtcNow;
var elapsed = (now - _lastRequestTime).TotalMilliseconds;
if (elapsed < minIntervalMs)
{
var delay = minIntervalMs - (int)elapsed;
_logger.LogDebug("Rate limit throttling: delaying for {Delay}ms (Mode: {Mode})", delay, mode);
await Task.Delay(delay);
}
_lastRequestTime = DateTime.UtcNow;
}
finally
{
_rateLimitSemaphore.Release();
}
}
}