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);
}
}
}