9cad9ed0fc
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
70 lines
3.0 KiB
C#
70 lines
3.0 KiB
C#
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
|
|
|
/// <summary>
|
|
/// Background service that periodically evicts idle browse sessions from the
|
|
/// <see cref="BrowseSessionRegistry"/>. Each tick takes a snapshot of the registry,
|
|
/// removes any session that has been idle longer than <see cref="IdleTtl"/>, then
|
|
/// disposes the evicted instance OUTSIDE the dictionary — so concurrent expand
|
|
/// calls racing eviction fail cleanly via <see cref="BrowseSessionNotFoundException"/>.
|
|
/// </summary>
|
|
public sealed class BrowseSessionReaper(
|
|
BrowseSessionRegistry registry,
|
|
ILogger<BrowseSessionReaper> logger) : BackgroundService
|
|
{
|
|
/// <summary>How long a session may be untouched before it becomes eligible for eviction.</summary>
|
|
public static readonly TimeSpan IdleTtl = TimeSpan.FromMinutes(2);
|
|
|
|
/// <summary>How often the reaper checks the registry.</summary>
|
|
public static readonly TimeSpan TickInterval = TimeSpan.FromSeconds(30);
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <summary>Evicts every session whose <see cref="Commons.Browsing.IBrowseSession.LastUsedUtc"/>
|
|
/// is older than <see cref="IdleTtl"/>. Internal so tests can drive a tick directly.</summary>
|
|
/// <param name="ct">Cancellation token for the reap pass.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
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);
|
|
}
|
|
}
|
|
}
|