using System.Collections.Concurrent; namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// Process-singleton cache of instances keyed on /// (ClusterId, GenerationId). Hot-path evaluation reads /// without awaiting DB access; the cache is populated /// out-of-band on publish + on first reference via /// . /// /// /// Per decision #148 and Phase 6.2 Stream B.4 the cache is generation-sealed: once a /// trie is installed for (ClusterId, GenerationId) the entry is immutable. When a /// new generation publishes, the caller calls with the new trie /// + the cache atomically updates its "current generation" pointer for that cluster. /// Older generations are retained so an in-flight request evaluating the prior generation /// still succeeds — GC via . /// public sealed class PermissionTrieCache { private readonly ConcurrentDictionary _byCluster = new(StringComparer.OrdinalIgnoreCase); /// Install a trie for a cluster + make it the current generation. public void Install(PermissionTrie trie) { ArgumentNullException.ThrowIfNull(trie); _byCluster.AddOrUpdate(trie.ClusterId, _ => ClusterEntry.FromSingle(trie), (_, existing) => existing.WithAdditional(trie)); } /// Get the current-generation trie for a cluster; null when nothing installed. public PermissionTrie? GetTrie(string clusterId) { ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); return _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current : null; } /// Get a specific (cluster, generation) trie; null if that pair isn't cached. public PermissionTrie? GetTrie(string clusterId, long generationId) { if (!_byCluster.TryGetValue(clusterId, out var entry)) return null; return entry.Tries.TryGetValue(generationId, out var trie) ? trie : null; } /// The generation id the shortcut currently serves for a cluster. public long? CurrentGenerationId(string clusterId) => _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null; /// Drop every cached trie for one cluster. public void Invalidate(string clusterId) => _byCluster.TryRemove(clusterId, out _); /// /// Retain only the most-recent generations for a cluster. /// No-op when there's nothing to drop. /// public void Prune(string clusterId, int keepLatest = 3) { if (keepLatest < 1) throw new ArgumentOutOfRangeException(nameof(keepLatest), keepLatest, "keepLatest must be >= 1"); if (!_byCluster.TryGetValue(clusterId, out var entry)) return; if (entry.Tries.Count <= keepLatest) return; var keep = entry.Tries .OrderByDescending(kvp => kvp.Key) .Take(keepLatest) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _byCluster[clusterId] = new ClusterEntry(entry.Current, keep); } /// Diagnostics counter: number of cached (cluster, generation) tries. public int CachedTrieCount => _byCluster.Values.Sum(e => e.Tries.Count); private sealed record ClusterEntry(PermissionTrie Current, IReadOnlyDictionary Tries) { public static ClusterEntry FromSingle(PermissionTrie trie) => new(trie, new Dictionary { [trie.GenerationId] = trie }); public ClusterEntry WithAdditional(PermissionTrie trie) { var next = new Dictionary(Tries) { [trie.GenerationId] = trie }; // The highest generation wins as "current" — handles out-of-order installs. var current = trie.GenerationId >= Current.GenerationId ? trie : Current; return new ClusterEntry(current, next); } } }