using System.Collections.Generic; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace GB5Shared.GOP.Qualifier { // ============================================================ // IQualifierCacheInvalidator // ============================================================ public interface IQualifierCacheInvalidator { /// /// Removes the compiled plan snapshot from L2 IMemoryCache /// for the specified qualifier + tenant combination. /// Called after ActivateQualifierVersion commits. /// void InvalidateQualifier(int qualifierId, int clientId); /// /// Removes all cached plans for a given tenant client. /// void InvalidateAllForClient(int clientId); /// Registers a cache key so tenant-wide invalidation can reach it. void Register(int qualifierId, int clientId); } // ============================================================ // QualifierCacheInvalidator — IMemoryCache implementation // // Cache keys follow the pattern: // "QualifierPlan:{clientId}:{qualifierId}" // // Since IMemoryCache has no tag-based eviction, tenant-wide // invalidation tracks registered keys in a ConcurrentDictionary // keyed by clientId. // ============================================================ public class QualifierCacheInvalidator : IQualifierCacheInvalidator { private readonly IMemoryCache _Cache; private readonly ILogger _Logger; // clientId → set of cache keys registered for that client private readonly System.Collections.Concurrent.ConcurrentDictionary> _KeysByClient = new(); public QualifierCacheInvalidator( IMemoryCache cache, ILogger logger) { _Cache = cache; _Logger = logger; } public void InvalidateQualifier(int qualifierId, int clientId) { var key = PlanKey(qualifierId, clientId); _Cache.Remove(key); if (_KeysByClient.TryGetValue(clientId, out var keys)) keys.Remove(key); _Logger.LogInformation( "QualifierCache: invalidated plan for qualifier {QualifierId} client {ClientId}", qualifierId, clientId); } public void InvalidateAllForClient(int clientId) { if (!_KeysByClient.TryRemove(clientId, out var keys)) return; foreach (var key in keys) _Cache.Remove(key); _Logger.LogInformation( "QualifierCache: invalidated {Count} plan entries for client {ClientId}", keys.Count, clientId); } /// Registers a cache key so tenant-wide invalidation can reach it. public void Register(int qualifierId, int clientId) { var keys = _KeysByClient.GetOrAdd(clientId, _ => []); lock (keys) keys.Add(PlanKey(qualifierId, clientId)); } public static string PlanKey(int qualifierId, int clientId) => $"QualifierPlan:{clientId}:{qualifierId}"; } }