From 9abda9c219213a3d063f8d154e2e772c62641601 Mon Sep 17 00:00:00 2001 From: kittapath Date: Sat, 18 Oct 2025 20:49:19 +0700 Subject: [PATCH] add time out --- Services/RecruitService.cs | 66 +++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/Services/RecruitService.cs b/Services/RecruitService.cs index 1a79172..620ac18 100644 --- a/Services/RecruitService.cs +++ b/Services/RecruitService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -23,6 +24,7 @@ namespace BMA.EHR.Recruit.Service.Services private readonly MetadataDbContext _contextMetadata; private readonly OrgDbContext _contextOrg; private readonly MinIOService _minIOService; + private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IConfiguration _configuration; @@ -31,12 +33,14 @@ namespace BMA.EHR.Recruit.Service.Services OrgDbContext contextOrg, IHttpContextAccessor httpContextAccessor, MinIOService minIOService, - IConfiguration configuration) + IConfiguration configuration, + IHttpClientFactory httpClientFactory) { _context = context; _contextMetadata = contextMetadata; _contextOrg = contextOrg; _minIOService = minIOService; + _httpClientFactory = httpClientFactory; _httpContextAccessor = httpContextAccessor; _configuration = configuration; } @@ -180,13 +184,25 @@ namespace BMA.EHR.Recruit.Service.Services { try { - // 🚀 Prepare HTTP client once - var httpClient1 = new HttpClient(); - httpClient1.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); - httpClient1.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); + // 🚀 Prepare HTTP client once via factory with timeout + var clientForPos = _httpClientFactory.CreateClient("default"); + clientForPos.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); + clientForPos.DefaultRequestHeaders.Remove("api_key"); + clientForPos.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"] ?? ""); var apiUrl1 = $"{_configuration["API"]}/org/pos/level"; - var response1 = await httpClient1.GetStringAsync(apiUrl1); - var posOptions = JsonConvert.DeserializeObject(response1); + var response1 = string.Empty; + 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(response1); var recruitImport = await _context.RecruitImports.AsQueryable() .FirstOrDefaultAsync(x => x.Id == examId); @@ -245,26 +261,41 @@ namespace BMA.EHR.Recruit.Service.Services .Where(x => !string.IsNullOrWhiteSpace(x.ExamId)) .ToDictionary(x => x.ExamId, x => x); - // 🚀 Prepare HTTP client once - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); - httpClient.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); + // 🚀 Batch HTTP requests using IHttpClientFactory with concurrency limit and cancellation + var clientForOrg = _httpClientFactory.CreateClient("default"); + clientForOrg.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); + 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 => { if (string.IsNullOrWhiteSpace(candidate.CitizenId)) return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; - var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; + await semaphore.WaitAsync(); try { - var response = await httpClient.GetStringAsync(apiUrl); - return new { CitizenId = candidate.CitizenId, org = JsonConvert.DeserializeObject(response) }; + var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; + 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(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(); @@ -445,7 +476,6 @@ namespace BMA.EHR.Recruit.Service.Services // 🚀 Single SaveChanges at the end await _contextMetadata.SaveChangesAsync(); - httpClient.Dispose(); } catch {