fix: reuse KIS tokens across concurrent requests
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
@@ -29,6 +31,7 @@ public class KisApiClient : IKisApiClient
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ITokenCache _tokenCache;
|
private readonly ITokenCache _tokenCache;
|
||||||
private readonly ILogger<KisApiClient> _logger;
|
private readonly ILogger<KisApiClient> _logger;
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> TokenLocks = new();
|
||||||
|
|
||||||
public KisApiClient(HttpClient httpClient, ITokenCache tokenCache, ILogger<KisApiClient> logger)
|
public KisApiClient(HttpClient httpClient, ITokenCache tokenCache, ILogger<KisApiClient> logger)
|
||||||
{
|
{
|
||||||
@@ -159,40 +162,54 @@ public class KisApiClient : IKisApiClient
|
|||||||
|
|
||||||
private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
|
private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
|
||||||
{
|
{
|
||||||
var cachedToken = await _tokenCache.GetCachedTokenAsync(creds.Account);
|
var tokenLock = TokenLocks.GetOrAdd(creds.Account, _ => new SemaphoreSlim(1, 1));
|
||||||
if (!string.IsNullOrEmpty(cachedToken))
|
await tokenLock.WaitAsync();
|
||||||
return cachedToken;
|
|
||||||
|
|
||||||
var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _httpClient.PostAsJsonAsync(
|
// Re-check after acquiring the account lock. Another request may
|
||||||
$"{creds.Domain}/oauth2/tokenP",
|
// have refreshed the shared cache while this request was waiting.
|
||||||
tokenRequest
|
var cachedToken = await _tokenCache.GetCachedTokenAsync(creds.Account);
|
||||||
);
|
if (!string.IsNullOrEmpty(cachedToken))
|
||||||
response.EnsureSuccessStatusCode();
|
return cachedToken;
|
||||||
|
|
||||||
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };
|
||||||
if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
|
|
||||||
|
|
||||||
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
|
try
|
||||||
throw new InvalidOperationException("No access_token in response");
|
{
|
||||||
var accessToken = tokenObj.ToString()!;
|
var response = await _httpClient.PostAsJsonAsync(
|
||||||
|
$"{creds.Domain}/oauth2/tokenP",
|
||||||
|
tokenRequest
|
||||||
|
);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
|
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||||
? expiresObj.ToString()
|
if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
|
||||||
: "86400";
|
|
||||||
var expiresInSec = int.TryParse(expiresInStr, out var seconds) ? seconds : 86400;
|
|
||||||
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
|
|
||||||
|
|
||||||
await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt);
|
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
|
||||||
return accessToken;
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
finally
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "KIS token refresh failed");
|
tokenLock.Release();
|
||||||
throw new InvalidOperationException("KIS token refresh failed; check credentials and API availability.", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using QuantEngine.Core.Interfaces;
|
using QuantEngine.Core.Interfaces;
|
||||||
@@ -31,7 +32,11 @@ namespace QuantEngine.Infrastructure.Services
|
|||||||
if (token == null)
|
if (token == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var expiresAt = DateTime.Parse(token.ExpiresAt);
|
DateTime expiresAt;
|
||||||
|
if (!DateTime.TryParse((string)token.ExpiresAt, CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||||
|
out expiresAt))
|
||||||
|
return null;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var refreshSkew = TimeSpan.FromMinutes(TokenRefreshSkewMinutes);
|
var refreshSkew = TimeSpan.FromMinutes(TokenRefreshSkewMinutes);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user