using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing; /// /// Background service that periodically evicts idle browse sessions from the /// . Each tick takes a snapshot of the registry, /// removes any session that has been idle longer than , then /// disposes the evicted instance OUTSIDE the dictionary — so concurrent expand /// calls racing eviction fail cleanly via . /// public sealed class BrowseSessionReaper( BrowseSessionRegistry registry, ILogger logger) : BackgroundService { /// How long a session may be untouched before it becomes eligible for eviction. public static readonly TimeSpan IdleTtl = TimeSpan.FromMinutes(2); /// How often the reaper checks the registry. public static readonly TimeSpan TickInterval = TimeSpan.FromSeconds(30); /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TickInterval); while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) { try { await ReapOnceAsync(stoppingToken).ConfigureAwait(false); } catch (Exception ex) { logger.LogWarning(ex, "Browse-session reaper iteration failed; will retry next tick."); } } await DrainAllAsync().ConfigureAwait(false); } /// Evicts every session whose /// is older than . Internal so tests can drive a tick directly. /// Cancellation token for the reap pass. /// A task that represents the asynchronous operation. internal async Task ReapOnceAsync(CancellationToken ct) { var now = DateTime.UtcNow; foreach (var (token, session) in registry.Snapshot()) { if (now - session.LastUsedUtc < IdleTtl) continue; if (!registry.TryRemove(token, out var taken)) continue; try { await taken.DisposeAsync().ConfigureAwait(false); } catch (Exception ex) { logger.LogDebug(ex, "Best-effort dispose of idle-evicted browse session {Token} failed.", token); } logger.LogDebug("Browse session {Token} closed reason=idle-ttl", token); } } private async Task DrainAllAsync() { foreach (var (token, session) in registry.Snapshot()) { if (!registry.TryRemove(token, out var taken)) continue; try { await taken.DisposeAsync().ConfigureAwait(false); } catch { } logger.LogDebug("Browse session {Token} closed reason=shutdown", token); } } }