e16146fa0c
Every picker and RawBrowseModal stored the OpenAsync token after the await; if the host was disposed (or the modal closed) meanwhile, its teardown had already run with no token, so the plant-side session lived until the reaper's idle TTL. BrowseOpenGuard.CloseIfOrphaned closes it at once; each host sets _disposed first thing in DisposeAsync (RawBrowseModal also treats !Visible as gone). Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
45 lines
2.0 KiB
C#
45 lines
2.0 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
|
|
|
/// <summary>
|
|
/// Closes a browse session that finished opening after its host went away (R04 L4). Every picker and
|
|
/// <c>RawBrowseModal</c> awaited <see cref="IBrowserSessionService.OpenAsync"/> and then stored the token;
|
|
/// if the host was disposed (or the modal closed) meanwhile, its teardown had already run with no token,
|
|
/// so the session — an OPC UA client session, an MQTT connection, a SQL connection against the plant —
|
|
/// stayed open until the reaper's idle TTL (2 min + one 30 s tick).
|
|
/// </summary>
|
|
public static class BrowseOpenGuard
|
|
{
|
|
/// <summary>
|
|
/// Call right after <see cref="IBrowserSessionService.OpenAsync"/> returns. When the host is gone it
|
|
/// closes the new session (fire-and-forget, like the hosts' own teardown) and returns
|
|
/// <see langword="true"/>, telling the caller not to adopt the token.
|
|
/// </summary>
|
|
/// <param name="result">The open result.</param>
|
|
/// <param name="hostGone">Whether the host was disposed (or closed) while the open was in flight.</param>
|
|
/// <param name="service">The browse service the session was opened on.</param>
|
|
/// <returns><see langword="true"/> when the caller must drop the result.</returns>
|
|
public static bool CloseIfOrphaned(BrowseOpenResult result, bool hostGone, IBrowserSessionService service)
|
|
{
|
|
if (!hostGone) return false;
|
|
if (result.Ok && result.Token != Guid.Empty)
|
|
{
|
|
// Never block (or throw into) a dead host's continuation on a slow remote close.
|
|
_ = CloseQuietlyAsync(service, result.Token);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static async Task CloseQuietlyAsync(IBrowserSessionService service, Guid token)
|
|
{
|
|
try
|
|
{
|
|
await service.CloseAsync(token).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// The reaper still bounds a session whose close failed.
|
|
}
|
|
}
|
|
}
|