fix(otopcua): skip redundant discovered-node re-apply + guard tests
This commit is contained in:
@@ -620,10 +620,31 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
var plan = DiscoveredNodeMapper.Map(equipmentId, msg.Nodes, authoredRefs);
|
||||
if (plan.Variables.Count == 0) return; // nothing new to inject (all captured nodes were authored)
|
||||
|
||||
// Unchanged-plan short-circuit: Task 6's driver re-discovers every ~2s (up to ~15 passes) until the
|
||||
// FixedTree set stabilises, re-sending DiscoveredNodesReady each pass. Re-applying an IDENTICAL plan
|
||||
// would re-send SetDesiredSubscriptions, forcing the child to UnsubscribeAsync (dropping the WHOLE
|
||||
// handle — authored tags included) then re-Subscribe — blipping authored-tag values up to ~15× across
|
||||
// the discovery window. Skip when the routing is unchanged from the last applied pass; a GROWING set
|
||||
// still differs (superset) and re-applies. This is _discoveredByDriver's first reader.
|
||||
if (_discoveredByDriver.TryGetValue(msg.DriverInstanceId, out var cached)
|
||||
&& RoutingEquals(cached.RoutingByRef, plan.RoutingByRef))
|
||||
{
|
||||
_log.Debug("DriverHost {Node}: discovered set for driver {Driver} unchanged ({Count} node(s)) — re-apply skipped",
|
||||
_localNode, msg.DriverInstanceId, plan.Variables.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
_discoveredByDriver[msg.DriverInstanceId] = plan;
|
||||
ApplyDiscoveredPlan(msg.DriverInstanceId, equipmentId, plan);
|
||||
}
|
||||
|
||||
/// <summary>Routing-map equality: same count + every key maps to the same NodeId. Lets
|
||||
/// <see cref="HandleDiscoveredNodes"/> skip re-applying an unchanged discovered set across the driver's
|
||||
/// repeated post-connect re-discovery passes (a grown/changed set differs and re-applies).</summary>
|
||||
private static bool RoutingEquals(IReadOnlyDictionary<string, string> a, IReadOnlyDictionary<string, string> b)
|
||||
=> a.Count == b.Count
|
||||
&& a.All(kv => b.TryGetValue(kv.Key, out var v) && string.Equals(v, kv.Value, StringComparison.Ordinal));
|
||||
|
||||
/// <summary>
|
||||
/// Grafts a <see cref="DiscoveredInjectionPlan"/> onto the served state: extends the live-value
|
||||
/// routing map (mirroring <see cref="PushDesiredSubscriptions"/>' fan-out so <see cref="ForwardToMux"/>
|
||||
@@ -636,7 +657,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
/// </summary>
|
||||
private void ApplyDiscoveredPlan(string driverId, string equipmentId, DiscoveredInjectionPlan plan)
|
||||
{
|
||||
// Extend the live-value routing map (fan-out), mirroring PushDesiredSubscriptions' pattern.
|
||||
// Extend the live-value routing map (fan-out), mirroring PushDesiredSubscriptions' pattern. This is
|
||||
// purely ADDITIVE across passes: a shrinking discovery set would leave the dropped refs' stale routes
|
||||
// until the next full apply (PushDesiredSubscriptions) clears + rebuilds the maps — acceptable because
|
||||
// a FOCAS FixedTree only grows-then-stabilises, never shrinks within a connect.
|
||||
foreach (var (driverRef, nodeId) in plan.RoutingByRef)
|
||||
{
|
||||
var key = (driverId, driverRef);
|
||||
@@ -654,6 +678,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
// and ForwardToMux routes the values. Recompute the authored value + alarm refs the same way
|
||||
// PushDesiredSubscriptions does, then union the FixedTree refs onto the value set.
|
||||
if (!_children.TryGetValue(driverId, out var entry)) return;
|
||||
// The _lastComposition null-guards below are defensive: HandleDiscoveredNodes already proved it
|
||||
// non-null, but Task 8 will also call ApplyDiscoveredPlan from the PushDesiredSubscriptions tail —
|
||||
// keep them so that re-apply path can't NRE.
|
||||
var authoredValueRefs = _lastComposition is null
|
||||
? Enumerable.Empty<string>()
|
||||
: _lastComposition.EquipmentTags
|
||||
@@ -899,6 +926,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
Receive<RouteNativeAlarmAck>(msg =>
|
||||
_log.Debug("DriverHost {Node}: dropping native-alarm ack for {Node2} while Stale (config DB unreachable)",
|
||||
_localNode, msg.ConditionNodeId));
|
||||
// A driver child's post-connect DiscoveredNodesReady can't be injected while Stale (no composition is
|
||||
// applied yet, so the equipment can't be resolved). Drop it — Task 6's re-discovery loop re-sends it
|
||||
// and the Task-8 post-recovery re-apply self-heal it once an apply runs (matches the no-op drops above).
|
||||
Receive<DriverInstanceActor.DiscoveredNodesReady>(_ => { });
|
||||
Receive<SubscribeAck>(_ => { /* PubSub ack */ });
|
||||
Timers.StartPeriodicTimer("retry-db", RetryConfigDbConnection.Instance, ReconnectInterval);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user