refactor(drivers): delete the dead discovered-node injection path (§8.2, #507)
#507 was filed as "injection inert in v3, re-migrate onto the raw subtree". Reading it, the retained code is not revivable: it resolves equipment from EquipmentNode.DriverInstanceId UNION EquipmentTags, and BOTH are structurally empty in v3 (AddressSpaceComposer always constructs EquipmentNode with a null DriverInstanceId; DeploymentArtifact hard-codes an empty EquipmentTags set). Removing the guard would have changed a log line and injected nothing. So it is deleted rather than fixed, and #507 closes as superseded by /raw browse-commit. Removed: HandleDiscoveredNodes, PartitionDiscoveredByDeviceHost, ShouldWarnPartition, PlansRoutingEqual, ApplyDiscoveredPlansForDriver, _discoveredByDriver, the redeploy re-inject tail, DiscoveredNodeMapper, DiscoveredInjection, AddressSpaceApplier.MaterialiseDiscoveredNodes, OpcUaPublishActor.MaterialiseDiscoveredNodes, and the Runtime-local copies of CapturingAddressSpaceBuilder/DiscoveredNode. The connect-time discovery loop goes too (StartDiscovery, RediscoverTick, HandleRediscoverAsync, DiscoveredNodesReady, TriggerRediscovery). With injection gone it had no consumer, and leaving it would either dead-letter or keep browsing real devices up to ~15x per connect to drop the result. ITagDiscovery itself stays — the /raw browse picker drives it through Commons/Browsing/DiscoveryDriverBrowser, which never went through the actor, so the picker is unaffected. deferment.md §4.4 called the 18 DiscoveryInjectionDormantV3 tests "Real — blocked on #507". They are not: they assert an equipment-rooted graft (EquipmentRootNodeId == "EQ-1") that v3 cannot produce, so they could never have unskipped as written. Deleted along with DiscoveredNodeMapperTests, the CapturingAddressSpaceBuilder tests, and the MaterialiseDiscoveredNodes tests in AddressSpaceApplierTests/OpcUaPublishActorTests. Runtime.Tests skipped: 31 -> 13. IHostConnectivityProbe is deliberately NOT resolved here. GetHostStatuses() has no production call site and nothing writes a DriverHostStatus row, but the table was re-created in the v3 initial migration, so deleting on inference would be wrong. Split to Gitea #521 with both options costed. CLAUDE.md, docs/drivers/{Galaxy,TwinCAT,MTConnect}.md updated — they described the seam as dead. Build clean; Runtime.Tests 476 passed / 13 skipped; OpcUaServer.Tests 362 passed / 4 skipped.
This commit is contained in:
@@ -32,16 +32,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
public static readonly TimeSpan DefaultReconnectInterval = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>Default interval between bounded post-connect re-discovery passes.</summary>
|
||||
public static readonly TimeSpan DefaultRediscoverInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Default cap on the number of post-connect re-discovery passes.</summary>
|
||||
public const int DefaultRediscoverMaxAttempts = 15;
|
||||
|
||||
/// <summary>Default per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/> during
|
||||
/// bounded post-connect re-discovery. Bounds the mailbox suspension time; production default 30 s.</summary>
|
||||
public static readonly TimeSpan DefaultRediscoverDiscoverTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public sealed record InitializeRequested(string DriverConfigJson);
|
||||
public sealed record InitializeSucceeded(int Generation);
|
||||
public sealed record InitializeFailed(string Reason, int Generation);
|
||||
@@ -124,21 +114,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// subscription that un-gates an <see cref="IAlarmSource"/> driver's feed. Handled async so the
|
||||
/// <see cref="IAlarmSource.SubscribeAlarmsAsync"/> call is bounded + off the synchronous handlers.</summary>
|
||||
private sealed record SubscribeAlarms;
|
||||
/// <summary>Published to the parent (DriverHostActor) after each post-connect discovery pass so it can
|
||||
/// graft the driver's discovered FixedTree nodes under the equipment. Empty/duplicate sets are fine —
|
||||
/// the parent dedups and injection is idempotent.</summary>
|
||||
public sealed record DiscoveredNodesReady(string DriverInstanceId, IReadOnlyList<DiscoveredNode> Nodes);
|
||||
|
||||
/// <summary>
|
||||
/// Sent by <see cref="DriverHostActor"/> to ask this driver child to re-run post-connect discovery
|
||||
/// after the host rebinds the driver to a new equipment. Handled only in <c>Connected</c>, where it
|
||||
/// re-kicks <see cref="StartDiscovery"/> — which already honours the driver's
|
||||
/// <see cref="ITagDiscovery.RediscoverPolicy"/> and the <see cref="ITagDiscovery"/> guard, tagging the
|
||||
/// fresh pass with the current init generation. In any non-Connected state it is a deliberate no-op:
|
||||
/// the driver's eventual (re)connect re-discovers anyway, so there is nothing to do and nothing to log.
|
||||
/// </summary>
|
||||
public sealed record TriggerRediscovery;
|
||||
|
||||
/// <summary>Self-sent when the wrapped driver raises <see cref="IRediscoverable.OnRediscoveryNeeded"/> —
|
||||
/// it observed that the remote's tag set may have changed. Marshals the event off the driver's thread
|
||||
/// onto the actor thread. Handled in EVERY behaviour, including Stubbed and Reconnecting: the raise has no
|
||||
@@ -146,10 +121,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// between connects), and dropping it in one state would lose the signal silently.</summary>
|
||||
private sealed record RediscoveryRaised(RediscoveryEventArgs Args);
|
||||
|
||||
/// <summary>Internal self-tick driving bounded post-connect re-discovery (FixedTree populates ~0–2s after connect).
|
||||
/// <paramref name="PreviousSignature"/> is the ordered-distinct full-reference signature of the prior pass's
|
||||
/// captured set (empty string on the first tick); re-discovery stops once a non-empty set repeats it.</summary>
|
||||
private sealed record RediscoverTick(int Generation, int Attempt, string PreviousSignature);
|
||||
public sealed class RetryConnect
|
||||
{
|
||||
public static readonly RetryConnect Instance = new();
|
||||
@@ -174,19 +145,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
private readonly IDriverHealthPublisher _healthPublisher;
|
||||
private readonly TimeSpan _reconnectInterval;
|
||||
|
||||
/// <summary>Interval between bounded post-connect re-discovery passes. Production default 2s; tests
|
||||
/// inject a tiny value so the loop runs without real-time waits.</summary>
|
||||
private readonly TimeSpan _rediscoverInterval;
|
||||
private readonly TimeSpan _healthPollInterval;
|
||||
|
||||
/// <summary>Cap on the number of post-connect re-discovery passes — a backstop so a never-stabilising
|
||||
/// (or perpetually-empty) discovered set cannot spin the loop forever. Production default 15.</summary>
|
||||
private readonly int _rediscoverMaxAttempts;
|
||||
|
||||
/// <summary>Per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/> during bounded post-connect
|
||||
/// re-discovery. Bounds the mailbox suspension time. Production default 30 s; tests may inject a shorter
|
||||
/// value. Stored to allow injection rather than hardcoding.</summary>
|
||||
private readonly TimeSpan _rediscoverDiscoverTimeout;
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
private string? _currentConfigJson;
|
||||
|
||||
@@ -252,9 +212,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// stub paths don't need to provide one.</param>
|
||||
/// <param name="clusterId">Optional cluster identifier forwarded in <see cref="DriverHealthChanged"/> messages;
|
||||
/// defaults to an empty string when not provided (e.g. in unit tests).</param>
|
||||
/// <param name="rediscoverInterval">Optional interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
||||
/// <param name="rediscoverMaxAttempts">Optional cap on re-discovery passes; defaults to 15.</param>
|
||||
/// <param name="rediscoverDiscoverTimeout">Optional per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/>; defaults to 30 seconds.</param>
|
||||
/// <param name="invoker">Optional Phase 6.1 resilience invoker wrapping this driver's capability
|
||||
/// calls; defaults to <see cref="NullDriverCapabilityInvoker"/> (pass-through) when not supplied.</param>
|
||||
/// <param name="healthPollInterval">Optional period of the health-poll heartbeat, which also drives the
|
||||
@@ -267,9 +224,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
bool startStubbed = false,
|
||||
IDriverHealthPublisher? healthPublisher = null,
|
||||
string? clusterId = null,
|
||||
TimeSpan? rediscoverInterval = null,
|
||||
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
|
||||
TimeSpan? rediscoverDiscoverTimeout = null,
|
||||
IDriverCapabilityInvoker? invoker = null,
|
||||
TimeSpan? healthPollInterval = null) =>
|
||||
Akka.Actor.Props.Create(() => new DriverInstanceActor(
|
||||
@@ -278,9 +232,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
startStubbed,
|
||||
healthPublisher ?? NullDriverHealthPublisher.Instance,
|
||||
clusterId ?? string.Empty,
|
||||
rediscoverInterval,
|
||||
rediscoverMaxAttempts,
|
||||
rediscoverDiscoverTimeout,
|
||||
invoker,
|
||||
healthPollInterval));
|
||||
|
||||
@@ -311,9 +262,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
/// <param name="startStubbed">If true, start in stub mode for testing or unavailable platforms.</param>
|
||||
/// <param name="healthPublisher">Sink for health-change notifications; must not be null.</param>
|
||||
/// <param name="clusterId">Cluster identifier forwarded in health snapshots.</param>
|
||||
/// <param name="rediscoverInterval">Interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
|
||||
/// <param name="rediscoverMaxAttempts">Cap on the number of re-discovery passes; defaults to 15.</param>
|
||||
/// <param name="rediscoverDiscoverTimeout">Per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/>; defaults to 30 seconds.</param>
|
||||
/// <param name="invoker">Phase 6.1 resilience invoker wrapping this driver's capability calls;
|
||||
/// defaults to <see cref="NullDriverCapabilityInvoker"/> (pass-through) when null.</param>
|
||||
/// <param name="healthPollInterval">Period of the health-poll heartbeat, which also drives the
|
||||
@@ -324,9 +272,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
bool startStubbed = false,
|
||||
IDriverHealthPublisher? healthPublisher = null,
|
||||
string? clusterId = null,
|
||||
TimeSpan? rediscoverInterval = null,
|
||||
int rediscoverMaxAttempts = DefaultRediscoverMaxAttempts,
|
||||
TimeSpan? rediscoverDiscoverTimeout = null,
|
||||
IDriverCapabilityInvoker? invoker = null,
|
||||
TimeSpan? healthPollInterval = null)
|
||||
{
|
||||
@@ -336,9 +281,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
_clusterId = clusterId ?? string.Empty;
|
||||
_healthPublisher = healthPublisher ?? NullDriverHealthPublisher.Instance;
|
||||
_reconnectInterval = reconnectInterval;
|
||||
_rediscoverInterval = rediscoverInterval ?? DefaultRediscoverInterval;
|
||||
_rediscoverMaxAttempts = rediscoverMaxAttempts;
|
||||
_rediscoverDiscoverTimeout = rediscoverDiscoverTimeout ?? DefaultRediscoverDiscoverTimeout;
|
||||
_healthPollInterval = healthPollInterval ?? HealthPollInterval;
|
||||
OtOpcUaTelemetry.DriverInstanceLifecycle.Add(1,
|
||||
new KeyValuePair<string, object?>("event", startStubbed ? "spawn_stub" : "spawn"),
|
||||
@@ -382,9 +324,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
Receive<SetDesiredSubscriptions>(StoreDesiredSubscriptions);
|
||||
// Stubbed drivers never enter Connected, so they never kick discovery; swallow defensively in case a
|
||||
// re-discovery self-tick is ever routed here so it doesn't surface as an Akka Unhandled message.
|
||||
Receive<RediscoverTick>(_ => { });
|
||||
// A TriggerRediscovery is meaningless to a stubbed (never-Connected) driver — silently ignore it.
|
||||
Receive<TriggerRediscovery>(_ => { });
|
||||
Receive<RediscoveryRaised>(HandleRediscoveryRaised);
|
||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||
}
|
||||
@@ -411,7 +350,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
ResubscribeDesired();
|
||||
AttachAlarmSource();
|
||||
SubscribeDesiredAlarms();
|
||||
StartDiscovery();
|
||||
});
|
||||
Receive<InitializeFailed>(msg =>
|
||||
{
|
||||
@@ -439,12 +377,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// A SubscribeAlarms self-tell (from Connected) can be overtaken by an already-queued disconnect into
|
||||
// this state; swallow it so it doesn't dead-letter — the next Connected entry re-subscribes.
|
||||
Receive<SubscribeAlarms>(_ => { });
|
||||
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
||||
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
||||
Receive<RediscoverTick>(_ => { });
|
||||
// A TriggerRediscovery arriving while not Connected is a deliberate no-op — the (re)connect path
|
||||
// re-runs discovery anyway. Swallow it so it stays a clean silent no-op (no Unhandled event).
|
||||
Receive<TriggerRediscovery>(_ => { });
|
||||
Receive<RediscoveryRaised>(HandleRediscoveryRaised);
|
||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||
}
|
||||
@@ -461,7 +393,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
_log.Warning("DriverInstance {Id}: disconnect observed ({Reason}); reconnecting",
|
||||
_driverInstanceId, msg.Reason);
|
||||
Timers.Cancel("rediscover");
|
||||
DetachSubscription();
|
||||
RecordFault();
|
||||
Become(Reconnecting);
|
||||
@@ -470,25 +401,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
Receive<ForceReconnect>(_ =>
|
||||
{
|
||||
_log.Info("DriverInstance {Id}: ForceReconnect requested by admin; re-entering Reconnecting", _driverInstanceId);
|
||||
Timers.Cancel("rediscover");
|
||||
DetachSubscription();
|
||||
Become(Reconnecting);
|
||||
PublishHealthSnapshot();
|
||||
});
|
||||
ReceiveAsync<RediscoverTick>(HandleRediscoverAsync);
|
||||
// The host asks for a fresh discovery pass after rebinding the driver to a new equipment. Cancel any
|
||||
// pending rediscover tick FIRST — mirroring ForceReconnect/DisconnectObserved — so a stale tick left
|
||||
// over from the prior loop can't fire alongside the freshly-kicked one, then re-kick the bounded loop
|
||||
// via StartDiscovery (honours RediscoverPolicy + the ITagDiscovery guard, tagged with the current
|
||||
// _initGeneration). Only handled here in Connected — non-Connected states no-op it below. A stale tick
|
||||
// that still slips through (one already mid-async-handler) is benign: the parent dedups
|
||||
// DiscoveredNodesReady and node injection is idempotent — the Cancel just avoids the avoidable double
|
||||
// pass in the common case.
|
||||
Receive<TriggerRediscovery>(_ =>
|
||||
{
|
||||
Timers.Cancel("rediscover");
|
||||
StartDiscovery();
|
||||
});
|
||||
ReceiveAsync<WriteAttribute>(HandleWriteAsync);
|
||||
ReceiveAsync<RouteAlarmAck>(HandleAcknowledgeAsync);
|
||||
ReceiveAsync<Subscribe>(HandleSubscribeAsync);
|
||||
@@ -602,7 +518,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
ResubscribeDesired();
|
||||
AttachAlarmSource();
|
||||
SubscribeDesiredAlarms();
|
||||
StartDiscovery(); // re-run discovery on reconnect — keeps the injected tree fresh if the backend's capabilities changed
|
||||
});
|
||||
// A failure here is a no-op regardless of generation — the retry timer keeps trying the
|
||||
// current config; only a (generation-matched) InitializeSucceeded transitions state.
|
||||
@@ -625,12 +540,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
// A SubscribeAlarms self-tell (from Connected) can be overtaken by an already-queued disconnect into
|
||||
// this state; swallow it so it doesn't dead-letter — the next Connected entry re-subscribes.
|
||||
Receive<SubscribeAlarms>(_ => { });
|
||||
// Likewise the attempt-0 re-discovery self-tick (sent on Connected entry) can be overtaken by an
|
||||
// already-queued disconnect; swallow it — the next Connected entry re-kicks discovery.
|
||||
Receive<RediscoverTick>(_ => { });
|
||||
// A TriggerRediscovery arriving while not Connected is a deliberate no-op — the (re)connect path
|
||||
// re-runs discovery anyway. Swallow it so it stays a clean silent no-op (no Unhandled event).
|
||||
Receive<TriggerRediscovery>(_ => { });
|
||||
Receive<RediscoveryRaised>(HandleRediscoveryRaised);
|
||||
Receive<HealthPollTick>(_ => PublishHealthSnapshot());
|
||||
Timers.StartPeriodicTimer("retry-connect", RetryConnect.Instance, _reconnectInterval);
|
||||
@@ -971,97 +880,6 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kick the bounded post-connect re-discovery loop on a <c>Connected</c> entry. A no-op unless the
|
||||
/// driver exposes <see cref="ITagDiscovery"/> (nothing to inject otherwise). Self-sends the first
|
||||
/// <see cref="RediscoverTick"/> tagged with the current init generation so a tick that outlives a reconnect
|
||||
/// is rejected by the generation guard in <see cref="HandleRediscoverAsync"/>.
|
||||
/// <para>Honours the driver's <see cref="ITagDiscovery.RediscoverPolicy"/>: <c>Never</c> opts out entirely
|
||||
/// (no tick scheduled); <c>Once</c> runs a single pass (the loop stops after the first publish in
|
||||
/// <see cref="HandleRediscoverAsync"/>); <c>UntilStable</c> retries each (re)connect, bounded by
|
||||
/// stop-on-stable (the discovered-set signature repeats) + the attempt cap.</para></summary>
|
||||
private void StartDiscovery()
|
||||
{
|
||||
if (_driver is not ITagDiscovery discovery) return; // driver doesn't expose discovery — nothing to inject
|
||||
if (discovery.RediscoverPolicy == DiscoveryRediscoverPolicy.Never)
|
||||
{
|
||||
// Driver opts out of post-connect discovery — don't even schedule the first tick.
|
||||
_log.Debug("DriverInstance {Id}: RediscoverPolicy=Never — skipping post-connect discovery", _driverInstanceId);
|
||||
return;
|
||||
}
|
||||
Self.Tell(new RediscoverTick(_initGeneration, Attempt: 0, PreviousSignature: string.Empty));
|
||||
}
|
||||
|
||||
/// <summary>Runs one post-connect discovery pass: captures the driver's streamed FixedTree via a
|
||||
/// <see cref="CapturingAddressSpaceBuilder"/> and ships the result to the parent as
|
||||
/// <see cref="DiscoveredNodesReady"/> (empty/duplicate sets are fine — the parent dedups and injection
|
||||
/// is idempotent). Retries on the <see cref="_rediscoverInterval"/> until the non-empty discovered SET
|
||||
/// has STABILISED (the ordered-distinct full-reference signature repeats — robust for incremental/paged
|
||||
/// browsers where a count alone could falsely settle a partial tree) or the <see cref="_rediscoverMaxAttempts"/>
|
||||
/// cap is hit, whichever comes first; keeps retrying while empty because a FOCAS-style FixedTree cache may
|
||||
/// still be populating.
|
||||
/// <para>Limitation: this assumes a driver's discovered set only GROWS toward a stable shape (true for
|
||||
/// FOCAS — its FixedTree appears once, and on the wonder deploy the driver-config <c>_options.Tags</c> is
|
||||
/// empty so the set is 0 until the cache populates). A driver that emits an initial non-empty set and
|
||||
/// later grows could stop early on a transient repeat; acceptable for current scope.</para></summary>
|
||||
private async Task HandleRediscoverAsync(RediscoverTick tick)
|
||||
{
|
||||
if (tick.Generation != _initGeneration) return; // stale (a reconnect superseded this pass)
|
||||
if (_driver is not ITagDiscovery discovery) return;
|
||||
|
||||
IReadOnlyList<DiscoveredNode> nodes;
|
||||
try
|
||||
{
|
||||
var builder = new CapturingAddressSpaceBuilder();
|
||||
// Bound the browse — ReceiveAsync suspends the mailbox for the whole handler, so an unbounded
|
||||
// DiscoverAsync would block DisconnectObserved / ForceReconnect / writes / health-poll behind it.
|
||||
using var cts = new CancellationTokenSource(_rediscoverDiscoverTimeout);
|
||||
// NO ConfigureAwait(false) on this outer await: a genuinely-async DiscoverAsync (Galaxy /
|
||||
// OpcUaClient / TwinCAT) must resume on the actor task scheduler so the Context.Parent.Tell +
|
||||
// Timers calls below run with a live ActorContext. ConfigureAwait(false) would resume
|
||||
// off-context and throw NotSupportedException("no active ActorContext"). The invoker's own
|
||||
// internal ConfigureAwait(false) does NOT propagate to this caller's await — the actor
|
||||
// continuation still resumes on the captured actor scheduler. (Discover retries per tier.)
|
||||
await _invoker.ExecuteAsync(
|
||||
DriverCapability.Discover,
|
||||
_driverInstanceId,
|
||||
async ct => await discovery.DiscoverAsync(builder, ct),
|
||||
cts.Token);
|
||||
nodes = builder.Nodes.ToArray(); // immutable snapshot — never hand the builder's live list across actors
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warning(ex, "DriverInstance {Id}: discovery pass {Attempt} failed; will retry", _driverInstanceId, tick.Attempt);
|
||||
nodes = Array.Empty<DiscoveredNode>();
|
||||
}
|
||||
|
||||
// Belt-and-suspenders: under ReceiveAsync the mailbox is suspended for the whole handler, so
|
||||
// _initGeneration cannot change mid-await — the pre-await guard + Timers.Cancel("rediscover") on
|
||||
// disconnect + single-timer key reuse are the primary protections. Re-checked in case that changes.
|
||||
if (tick.Generation != _initGeneration) return;
|
||||
|
||||
Context.Parent.Tell(new DiscoveredNodesReady(_driverInstanceId, nodes));
|
||||
|
||||
// Honour the driver's re-discovery policy. A Once driver runs a single post-connect pass per
|
||||
// (re)connect regardless of whether DiscoverAsync is synchronous or async — one published pass is
|
||||
// complete, so the retry loop is skipped (no further tick scheduled). (Never never reaches here —
|
||||
// StartDiscovery returns before the first tick.) UntilStable falls through to the stop-on-stable +
|
||||
// attempt-cap logic below.
|
||||
if (discovery.RediscoverPolicy == DiscoveryRediscoverPolicy.Once)
|
||||
{
|
||||
_log.Debug("DriverInstance {Id}: RediscoverPolicy=Once — single discovery pass, not scheduling another", _driverInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop when the non-empty discovered SET has stabilised (its signature repeats), or the attempt cap
|
||||
// is hit. Keep retrying while empty (a FixedTree cache may still be populating). First tick carries "".
|
||||
var signature = string.Join('\u0001',
|
||||
nodes.Select(n => n.FullReference).Distinct(StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal));
|
||||
var stableNonEmpty = nodes.Count > 0 && string.Equals(signature, tick.PreviousSignature, StringComparison.Ordinal);
|
||||
if (tick.Attempt + 1 < _rediscoverMaxAttempts && !stableNonEmpty)
|
||||
Timers.StartSingleTimer("rediscover", new RediscoverTick(tick.Generation, tick.Attempt + 1, signature), _rediscoverInterval);
|
||||
else
|
||||
_log.Debug("DriverInstance {Id}: discovery settled after {Attempt} pass(es), {Count} node(s)", _driverInstanceId, tick.Attempt + 1, nodes.Count);
|
||||
}
|
||||
|
||||
/// <summary>Records the host's desired subscription set without touching the live subscription.
|
||||
/// The set is (re)applied by <see cref="ResubscribeDesired"/> on the next <c>Connected</c> entry.</summary>
|
||||
|
||||
Reference in New Issue
Block a user