feat(browse): Wave-0 Batch E — UntilStable settle loop + BrowserSessionService fallback
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
the node set is non-empty and stable across two passes, bounded by open-timeout; on
timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).
This commit is contained in:
@@ -95,7 +95,7 @@ public sealed class DiscoveryDriverBrowser : IUniversalDriverBrowser
|
||||
openCts.CancelAfter(_openTimeout);
|
||||
|
||||
await driver.InitializeAsync(patched, openCts.Token).ConfigureAwait(false);
|
||||
return await DiscoverTreeAsync(disc, openCts.Token).ConfigureAwait(false);
|
||||
return await DiscoverTreeAsync(disc, driverType, openCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -103,14 +103,44 @@ public sealed class DiscoveryDriverBrowser : IUniversalDriverBrowser
|
||||
}
|
||||
}
|
||||
|
||||
// Task 9 replaces this with the UntilStable settle version.
|
||||
private static async Task<CapturedTree> DiscoverTreeAsync(ITagDiscovery disc, CancellationToken ct)
|
||||
private static readonly TimeSpan SettleInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
private async Task<CapturedTree> DiscoverTreeAsync(ITagDiscovery disc, string driverType, CancellationToken ct)
|
||||
{
|
||||
var builder = new CapturingAddressSpaceBuilder(NodeCap);
|
||||
if (disc.RediscoverPolicy != DiscoveryRediscoverPolicy.UntilStable)
|
||||
{
|
||||
var builder = new CapturingAddressSpaceBuilder(NodeCap);
|
||||
#pragma warning disable OTOPCUA0001 // One-shot browse capture, NOT a runtime dispatch path: the driver is a throwaway instance we construct/connect/discover/discard for the picker. CapabilityInvoker (retry/breaker/telemetry) governs live polling, not this admin-side capture.
|
||||
await disc.DiscoverAsync(builder, ct).ConfigureAwait(false);
|
||||
await disc.DiscoverAsync(builder, ct).ConfigureAwait(false);
|
||||
#pragma warning restore OTOPCUA0001
|
||||
return builder.Build();
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
// UntilStable (FOCAS): the discovered shape fills in asynchronously after connect —
|
||||
// re-capture until non-empty and stable across two consecutive passes, bounded by the
|
||||
// open-timeout. Mirrors the deploy-time contract in DriverInstanceActor.
|
||||
CapturedTree? previous = null;
|
||||
while (true)
|
||||
{
|
||||
var builder = new CapturingAddressSpaceBuilder(NodeCap);
|
||||
try
|
||||
{
|
||||
#pragma warning disable OTOPCUA0001 // One-shot browse capture, NOT a runtime dispatch path (see above).
|
||||
await disc.DiscoverAsync(builder, ct).ConfigureAwait(false);
|
||||
#pragma warning restore OTOPCUA0001
|
||||
var current = builder.Build();
|
||||
if (current.Count > 0 && previous?.Count == current.Count) return current;
|
||||
previous = current;
|
||||
await Task.Delay(SettleInterval, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (previous is { Count: > 0 })
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"UntilStable settle for {DriverType} hit the open-timeout before stabilising; returning the last capture ({Count} nodes)",
|
||||
driverType, previous.Count);
|
||||
return previous; // best effort: a fresh-but-unsettled tree beats a failed open
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BoundedShutdownAsync(IDriver driver, string driverType)
|
||||
|
||||
Reference in New Issue
Block a user