feat(web): implement exponential backoff retry pattern in KisApiClient
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 23s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled
Prepare Release / Build & Create Release (push) Successful in 1m3s
Prepare Release / Release Notification (push) Successful in 1s

This commit is contained in:
2026-07-12 13:16:38 +09:00
parent a3fe301308
commit 6db27fd634
@@ -141,6 +141,11 @@ public class KisApiClient : IKisApiClient
if (!string.IsNullOrEmpty(queryString)) if (!string.IsNullOrEmpty(queryString))
url += $"?{queryString}"; url += $"?{queryString}";
int maxAttempts = 3;
int delayMs = 1000;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try try
{ {
var request = new HttpRequestMessage(HttpMethod.Get, url); var request = new HttpRequestMessage(HttpMethod.Get, url);
@@ -153,13 +158,22 @@ public class KisApiClient : IKisApiClient
var result = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>(); var result = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
return result ?? new Dictionary<string, object>(); return result ?? new Dictionary<string, object>();
} }
catch (Exception ex) when (attempt < maxAttempts)
{
_logger.LogWarning(ex, "KIS request failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
await Task.Delay(delayMs);
delayMs *= 2;
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "KIS request failed: {Path} / {TrId}", path, trId); _logger.LogError(ex, "KIS request failed after {MaxAttempts} attempts: {Path} / {TrId}", maxAttempts, path, trId);
throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId}.", ex); throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId} after {maxAttempts} attempts.", ex);
} }
} }
throw new InvalidOperationException("Unreachable code in KIS client SendRequestAsync");
}
private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds) private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
{ {
var tokenLock = TokenLocks.GetOrAdd(creds.Account, _ => new SemaphoreSlim(1, 1)); var tokenLock = TokenLocks.GetOrAdd(creds.Account, _ => new SemaphoreSlim(1, 1));
@@ -174,6 +188,11 @@ public class KisApiClient : IKisApiClient
var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret }; var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };
int maxAttempts = 3;
int delayMs = 1000;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try try
{ {
var response = await _httpClient.PostAsJsonAsync( var response = await _httpClient.PostAsJsonAsync(
@@ -201,12 +220,20 @@ public class KisApiClient : IKisApiClient
_logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt); _logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt);
return accessToken; return accessToken;
} }
catch (Exception ex) when (attempt < maxAttempts)
{
_logger.LogWarning(ex, "KIS token refresh failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
await Task.Delay(delayMs);
delayMs *= 2;
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "KIS token refresh failed"); _logger.LogError(ex, "KIS token refresh failed after {MaxAttempts} attempts", maxAttempts);
throw new InvalidOperationException("KIS token refresh failed; check credentials and API availability.", ex); throw new InvalidOperationException($"KIS token refresh failed after {maxAttempts} attempts; check credentials and API availability.", ex);
} }
} }
throw new InvalidOperationException("Unreachable code in KIS client TokenRefresh");
}
finally finally
{ {
tokenLock.Release(); tokenLock.Release();