From 720a5e6091cfe4f955a64963ab94a7bfecd92535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Tue, 19 May 2026 19:59:44 +0200 Subject: [PATCH] fix(infra): use thread-safe Lazy Task for active AI request deduplication --- .../Services/KnowledgeService.cs | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/NexusReader.Infrastructure/Services/KnowledgeService.cs b/src/NexusReader.Infrastructure/Services/KnowledgeService.cs index 1ada2b1..5bfeda7 100644 --- a/src/NexusReader.Infrastructure/Services/KnowledgeService.cs +++ b/src/NexusReader.Infrastructure/Services/KnowledgeService.cs @@ -31,7 +31,7 @@ public class KnowledgeService : IKnowledgeService private readonly Tokenizer _tokenizer; private readonly ILogger _logger; private const string PromptVersion = "1.3"; - private static readonly ConcurrentDictionary>> _activeRequests = new(); + private static readonly ConcurrentDictionary>>> _activeRequests = new(); public KnowledgeService( IChatClient chatClient, @@ -100,10 +100,38 @@ public class KnowledgeService : IKnowledgeService // Deduplicate concurrent active requests for the exact same hash var requestKey = $"{tenantId}:{hash}:{traceType}"; - var task = _activeRequests.GetOrAdd(requestKey, _ => - ExecuteAiRequestAndCacheAsync(normalizedText, tenantId, systemPrompt, traceType, ebookId, hash)); + + var lazyTask = _activeRequests.GetOrAdd(requestKey, k => + new Lazy>>( + () => ExecuteAiRequestAndCacheAsync(normalizedText, tenantId, systemPrompt, traceType, ebookId, hash), + System.Threading.LazyThreadSafetyMode.ExecutionAndPublication + )); - return await task; + try + { + var result = await lazyTask.Value; + + // If the AI call returned a failure, remove it from the active dictionary + // so subsequent retries have a chance to request the AI again. + if (result.IsFailed) + { + _activeRequests.TryRemove(requestKey, out _); + } + + return result; + } + catch (Exception) + { + // Evict from active dictionary on hard exceptions to ensure system resiliency + _activeRequests.TryRemove(requestKey, out _); + throw; + } + finally + { + // Once a task successfully finishes and persists to the Persistent Database Cache, + // we evict it from RAM (_activeRequests) since future hits will leverage the DB cache. + _activeRequests.TryRemove(requestKey, out _); + } } private async Task> ExecuteAiRequestAndCacheAsync(