feat(browse): Wave-0 Batch F — capture coalescing/cap + AddAdminUI DI registration
Task 10: in-flight coalescing (Lazy<Task<CapturedTree>> keyed on driverType+SHA256(config))
so repeat Browse clicks share one device walk; global SemaphoreSlim cap
(MaxConcurrentCaptures=4) queues excess; per-caller ct via WaitAsync leaves the shared
capture (bounded by open-timeout, CancellationToken.None) unaffected; in-flight-only map
(Refresh = fresh capture). 7 concurrency tests green (158 total in Commons.Tests).
Task 12: AddAdminUI registers IUniversalDriverBrowser->DiscoveryDriverBrowser with
NullDriverFactory fallback (standalone AdminUI => CanBrowse false => manual entry).
AdminUI.Tests 542 green.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -18,6 +19,9 @@ public sealed class DiscoveryDriverBrowser : IUniversalDriverBrowser
|
||||
/// <summary>Node-count cap the capturing builder enforces before marking the tree truncated.</summary>
|
||||
public const int NodeCap = 50_000;
|
||||
|
||||
/// <summary>Global cap on simultaneous captures (§4.2). Coalesced awaiters don't hold a slot.</summary>
|
||||
public const int MaxConcurrentCaptures = 4;
|
||||
|
||||
/// <summary>Per-type declarative config patch guaranteeing browse mode at open time (§5.1).
|
||||
/// A dictionary entry per config-gated driver — not code.</summary>
|
||||
internal static readonly IReadOnlyDictionary<string, string> BrowsePatches =
|
||||
@@ -33,6 +37,8 @@ public sealed class DiscoveryDriverBrowser : IUniversalDriverBrowser
|
||||
private readonly ILogger<DiscoveryDriverBrowser> _logger;
|
||||
private readonly TimeSpan _openTimeout;
|
||||
private readonly TimeSpan _shutdownTimeout;
|
||||
private readonly ConcurrentDictionary<string, Lazy<Task<CapturedTree>>> _inflight = new();
|
||||
private readonly SemaphoreSlim _captureSlots = new(MaxConcurrentCaptures, MaxConcurrentCaptures);
|
||||
|
||||
/// <summary>Production constructor: standard open + shutdown bounds.</summary>
|
||||
/// <param name="driverFactory">Factory used to construct throwaway/capture driver instances.</param>
|
||||
@@ -75,10 +81,41 @@ public sealed class DiscoveryDriverBrowser : IUniversalDriverBrowser
|
||||
/// <inheritdoc />
|
||||
public async Task<IBrowseSession> OpenAsync(string driverType, string configJson, CancellationToken ct)
|
||||
{
|
||||
var tree = await CaptureAsync(driverType, configJson, ct).ConfigureAwait(false);
|
||||
var key = CaptureKey(driverType, configJson);
|
||||
// Lazy: exactly one RunCaptureAsync per key even under GetOrAdd races.
|
||||
var capture = _inflight.GetOrAdd(key,
|
||||
_ => new Lazy<Task<CapturedTree>>(() => RunCaptureAsync(key, driverType, configJson)));
|
||||
// The shared capture runs independent of any single caller (its own OpenTimeout bounds it);
|
||||
// each caller — including a queued one — observes ITS OWN ct via WaitAsync.
|
||||
var tree = await capture.Value.WaitAsync(ct).ConfigureAwait(false);
|
||||
return new CapturedTreeBrowseSession(tree);
|
||||
}
|
||||
|
||||
private async Task<CapturedTree> RunCaptureAsync(string key, string driverType, string configJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _captureSlots.WaitAsync().ConfigureAwait(false); // FIFO-ish queue for a slot
|
||||
try
|
||||
{
|
||||
return await CaptureAsync(driverType, configJson, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_captureSlots.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inflight.TryRemove(key, out _); // in-flight only — no result caching (Refresh = fresh capture)
|
||||
}
|
||||
}
|
||||
|
||||
internal static string CaptureKey(string driverType, string configJson) =>
|
||||
driverType.ToLowerInvariant() + ":" +
|
||||
Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes(configJson ?? "")));
|
||||
|
||||
private async Task<CapturedTree> CaptureAsync(string driverType, string configJson, CancellationToken ct)
|
||||
{
|
||||
var patched = PatchForBrowse(driverType, configJson);
|
||||
|
||||
Reference in New Issue
Block a user