perf(otopcua): one SetDesiredSubscriptions per driver per redeploy (follow-up D)

This commit is contained in:
Joseph Doherty
2026-06-26 14:30:16 -04:00
parent cde16063d9
commit 05c820795a
2 changed files with 203 additions and 0 deletions
@@ -1241,9 +1241,21 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
_driverRefByNodeId[nodeId] = key;
}
// Snapshot the cached (FixedTree-discovered) driver set BEFORE the bulk loop, while _discoveredByDriver
// is still untouched (the re-inject tail below drops/removes entries). Cached drivers are SKIPPED in the
// bulk loop because the tail sends each of them EXACTLY ONE SetDesiredSubscriptions for this pass: the
// authoreddiscovered union (ApplyDiscoveredPlansForDriver) for a survivor, or — if its plan is fully
// dropped — an authored-only fallback. Sending the bulk authored-only set HERE too would force the child
// to drop the whole handle (authored tags included) then re-subscribe — an extra unsub/resub blip of the
// authored values once per cached driver per redeploy. Net effect: exactly ONE send per driver per pass.
var cachedDriverIds = _discoveredByDriver.Keys.ToHashSet(StringComparer.Ordinal);
var total = 0;
foreach (var (driverId, entry) in _children)
{
// Cached drivers are owned exclusively by the re-inject tail (one send each) — skip here. Non-cached
// drivers keep the bulk authored-only send exactly as before.
if (cachedDriverIds.Contains(driverId)) continue;
var refs = refsByDriver.TryGetValue(driverId, out var r) ? r : Array.Empty<string>();
var alarmRefs = alarmRefsByDriver.TryGetValue(driverId, out var ar) ? ar : Array.Empty<string>();
entry.Actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(refs, SubscriptionPublishingInterval, alarmRefs));
@@ -1353,6 +1365,19 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
if (plansByEquipment.Count == 0)
{
_discoveredByDriver.Remove(driverId);
// FALLBACK (one-send invariant): this driver was SKIPPED in the bulk loop (it was cached), and its
// plan is now FULLY DROPPED — so ApplyDiscoveredPlansForDriver won't run for it and it would
// otherwise receive ZERO sends this pass, losing its AUTHORED subscriptions. Send the authored-only
// set NOW (the SAME payload the bulk loop computes), so the authored tags subscribe in THIS pass.
// (The TriggerRediscovery above handles the async FixedTree re-graft separately; this just keeps
// the authored values live meanwhile.) Guarded on the child still existing — a driver removed by
// ReconcileDrivers has no child and correctly gets no send.
if (_children.TryGetValue(driverId, out var fallbackEntry))
{
var refs = refsByDriver.TryGetValue(driverId, out var r) ? r : Array.Empty<string>();
var alarmRefs = alarmRefsByDriver.TryGetValue(driverId, out var ar) ? ar : Array.Empty<string>();
fallbackEntry.Actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(refs, SubscriptionPublishingInterval, alarmRefs));
}
continue;
}
ApplyDiscoveredPlansForDriver(driverId, plansByEquipment);