diff --git a/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs b/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs index ed4227b5..25d8af39 100644 --- a/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs +++ b/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs @@ -141,23 +141,37 @@ public class KisApiClient : IKisApiClient if (!string.IsNullOrEmpty(queryString)) url += $"?{queryString}"; - try - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in headers) - request.Headers.Add(header.Key, header.Value); + int maxAttempts = 3; + int delayMs = 1000; - var response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync>(); - return result ?? new Dictionary(); - } - catch (Exception ex) + for (int attempt = 1; attempt <= maxAttempts; attempt++) { - _logger.LogError(ex, "KIS request failed: {Path} / {TrId}", path, trId); - throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId}.", ex); + try + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in headers) + request.Headers.Add(header.Key, header.Value); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + return result ?? new Dictionary(); + } + 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) + { + _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} after {maxAttempts} attempts.", ex); + } } + + throw new InvalidOperationException("Unreachable code in KIS client SendRequestAsync"); } private async Task GetOrRefreshTokenAsync(KisCredentials creds) @@ -174,38 +188,51 @@ public class KisApiClient : IKisApiClient var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret }; - try + int maxAttempts = 3; + int delayMs = 1000; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) { - var response = await _httpClient.PostAsJsonAsync( - $"{creds.Domain}/oauth2/tokenP", - tokenRequest - ); - response.EnsureSuccessStatusCode(); + try + { + var response = await _httpClient.PostAsJsonAsync( + $"{creds.Domain}/oauth2/tokenP", + tokenRequest + ); + response.EnsureSuccessStatusCode(); - var tokenData = await response.Content.ReadFromJsonAsync>(); - if (tokenData == null) throw new InvalidOperationException("Token response body is empty"); + var tokenData = await response.Content.ReadFromJsonAsync>(); + if (tokenData == null) throw new InvalidOperationException("Token response body is empty"); - if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null) - throw new InvalidOperationException("No access_token in response"); - var accessToken = tokenObj.ToString()!; + if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null) + throw new InvalidOperationException("No access_token in response"); + var accessToken = tokenObj.ToString()!; - var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null - ? expiresObj.ToString() - : "86400"; - var expiresInSec = int.TryParse(expiresInStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds) - ? seconds - : 86400; - var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec); + var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null + ? expiresObj.ToString() + : "86400"; + var expiresInSec = int.TryParse(expiresInStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds) + ? seconds + : 86400; + var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec); - await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt); - _logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt); - return accessToken; - } - catch (Exception ex) - { - _logger.LogError(ex, "KIS token refresh failed"); - throw new InvalidOperationException("KIS token refresh failed; check credentials and API availability.", ex); + await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt); + _logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt); + 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) + { + _logger.LogError(ex, "KIS token refresh failed after {MaxAttempts} attempts", maxAttempts); + 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 {