feat(adminui): in-process browse session registry + TTL reaper + service
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IBrowserSessionService"/> implementation. Indexes injected
|
||||
/// <see cref="IDriverBrowser"/>s by <see cref="IDriverBrowser.DriverType"/>
|
||||
/// (case-insensitive) at construction, registers opened sessions, and wraps each
|
||||
/// expand/attributes call in a 20-second linked CTS so a stuck driver cannot
|
||||
/// stall the UI indefinitely.
|
||||
/// </summary>
|
||||
public sealed class BrowserSessionService(
|
||||
IEnumerable<IDriverBrowser> browsers,
|
||||
BrowseSessionRegistry registry,
|
||||
ILogger<BrowserSessionService> logger) : IBrowserSessionService
|
||||
{
|
||||
/// <summary>Upper bound on a single root/expand/attributes call.</summary>
|
||||
public static readonly TimeSpan PerCallTimeout = TimeSpan.FromSeconds(20);
|
||||
|
||||
private readonly IReadOnlyDictionary<string, IDriverBrowser> _browsersByType =
|
||||
browsers.ToDictionary(b => b.DriverType, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BrowseOpenResult> OpenAsync(string driverType, string configJson, CancellationToken ct)
|
||||
{
|
||||
if (!_browsersByType.TryGetValue(driverType, out var browser))
|
||||
return new(false, $"No browser registered for driver type '{driverType}'.", Guid.Empty);
|
||||
try
|
||||
{
|
||||
var session = await browser.OpenAsync(configJson, ct).ConfigureAwait(false);
|
||||
registry.Register(session);
|
||||
return new(true, null, session.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogInformation(ex,
|
||||
"Browser open failed for driverType={DriverType}: {Message}", driverType, ex.Message);
|
||||
return new(false, ex.Message, Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct) =>
|
||||
InvokeAsync(token, ct, (s, c) => s.RootAsync(c));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<BrowseNode>> ExpandAsync(Guid token, string nodeId, CancellationToken ct) =>
|
||||
InvokeAsync(token, ct, (s, c) => s.ExpandAsync(nodeId, c));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct) =>
|
||||
InvokeAsync<IReadOnlyList<AttributeInfo>>(token, ct, (s, c) => s.AttributesAsync(nodeId, c));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CloseAsync(Guid token)
|
||||
{
|
||||
if (!registry.TryRemove(token, out var session)) return;
|
||||
try { await session.DisposeAsync().ConfigureAwait(false); } catch { }
|
||||
logger.LogDebug("Browse session {Token} closed reason=user-close", token);
|
||||
}
|
||||
|
||||
private async Task<T> InvokeAsync<T>(
|
||||
Guid token, CancellationToken callerCt, Func<IBrowseSession, CancellationToken, Task<T>> op)
|
||||
{
|
||||
if (!registry.TryGet(token, out var session))
|
||||
throw new BrowseSessionNotFoundException(token);
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(callerCt);
|
||||
cts.CancelAfter(PerCallTimeout);
|
||||
return await op(session, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user