add time out

This commit is contained in:
kittapath 2025-10-18 20:49:19 +07:00
parent f6b1e7779d
commit 9abda9c219

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -23,6 +24,7 @@ namespace BMA.EHR.Recruit.Service.Services
private readonly MetadataDbContext _contextMetadata; private readonly MetadataDbContext _contextMetadata;
private readonly OrgDbContext _contextOrg; private readonly OrgDbContext _contextOrg;
private readonly MinIOService _minIOService; private readonly MinIOService _minIOService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
@ -31,12 +33,14 @@ namespace BMA.EHR.Recruit.Service.Services
OrgDbContext contextOrg, OrgDbContext contextOrg,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
MinIOService minIOService, MinIOService minIOService,
IConfiguration configuration) IConfiguration configuration,
IHttpClientFactory httpClientFactory)
{ {
_context = context; _context = context;
_contextMetadata = contextMetadata; _contextMetadata = contextMetadata;
_contextOrg = contextOrg; _contextOrg = contextOrg;
_minIOService = minIOService; _minIOService = minIOService;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_configuration = configuration; _configuration = configuration;
} }
@ -180,13 +184,25 @@ namespace BMA.EHR.Recruit.Service.Services
{ {
try try
{ {
// 🚀 Prepare HTTP client once // 🚀 Prepare HTTP client once via factory with timeout
var httpClient1 = new HttpClient(); var clientForPos = _httpClientFactory.CreateClient("default");
httpClient1.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); clientForPos.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", ""));
httpClient1.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); clientForPos.DefaultRequestHeaders.Remove("api_key");
clientForPos.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"] ?? "");
var apiUrl1 = $"{_configuration["API"]}/org/pos/level"; var apiUrl1 = $"{_configuration["API"]}/org/pos/level";
var response1 = await httpClient1.GetStringAsync(apiUrl1); var response1 = string.Empty;
var posOptions = JsonConvert.DeserializeObject<RecruitPosRequest>(response1); try
{
using var ctsPos = new CancellationTokenSource(TimeSpan.FromSeconds(30));
response1 = await clientForPos.GetStringAsync(apiUrl1, ctsPos.Token);
}
catch (TaskCanceledException)
{
// timeout - fallback to empty posOptions
response1 = string.Empty;
}
var posOptions = string.IsNullOrWhiteSpace(response1) ? null : JsonConvert.DeserializeObject<RecruitPosRequest>(response1);
var recruitImport = await _context.RecruitImports.AsQueryable() var recruitImport = await _context.RecruitImports.AsQueryable()
.FirstOrDefaultAsync(x => x.Id == examId); .FirstOrDefaultAsync(x => x.Id == examId);
@ -245,26 +261,41 @@ namespace BMA.EHR.Recruit.Service.Services
.Where(x => !string.IsNullOrWhiteSpace(x.ExamId)) .Where(x => !string.IsNullOrWhiteSpace(x.ExamId))
.ToDictionary(x => x.ExamId, x => x); .ToDictionary(x => x.ExamId, x => x);
// 🚀 Prepare HTTP client once // 🚀 Batch HTTP requests using IHttpClientFactory with concurrency limit and cancellation
var httpClient = new HttpClient(); var clientForOrg = _httpClientFactory.CreateClient("default");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); clientForOrg.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", ""));
httpClient.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); clientForOrg.DefaultRequestHeaders.Remove("api_key");
clientForOrg.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"] ?? "");
// 🚀 Batch HTTP requests var semaphore = new SemaphoreSlim(10); // limit concurrency
var orgTasks = candidates.Select(async candidate => var orgTasks = candidates.Select(async candidate =>
{ {
if (string.IsNullOrWhiteSpace(candidate.CitizenId)) if (string.IsNullOrWhiteSpace(candidate.CitizenId))
return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null };
var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; await semaphore.WaitAsync();
try try
{ {
var response = await httpClient.GetStringAsync(apiUrl); var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}";
return new { CitizenId = candidate.CitizenId, org = JsonConvert.DeserializeObject<dynamic>(response) }; try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await clientForOrg.GetStringAsync(apiUrl, cts.Token);
return new { CitizenId = candidate.CitizenId, org = JsonConvert.DeserializeObject<dynamic>(response) };
}
catch (TaskCanceledException)
{
// timeout
return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null };
}
catch (Exception)
{
return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null };
}
} }
catch finally
{ {
return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; semaphore.Release();
} }
}).ToList(); }).ToList();
@ -445,7 +476,6 @@ namespace BMA.EHR.Recruit.Service.Services
// 🚀 Single SaveChanges at the end // 🚀 Single SaveChanges at the end
await _contextMetadata.SaveChangesAsync(); await _contextMetadata.SaveChangesAsync();
httpClient.Dispose();
} }
catch catch
{ {