fix(r2-04): surface degraded address-space applies (Error log + otopcua.opcua.apply.failed)

This commit is contained in:
Joseph Doherty
2026-07-13 10:44:18 -04:00
parent e37abf36a0
commit 759125372a
4 changed files with 211 additions and 8 deletions
@@ -335,27 +335,43 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
var outcome = _applier.Apply(plan);
_lastApplied = composition;
_applier.MaterialiseHierarchy(composition);
// Sum swallowed per-node materialise failures across every pass together with Apply's own
// removal-pass tally (archreview 01/S-1). A degraded apply used to vanish into per-node
// Warnings + an optimistic Info line; now it surfaces at Error + a dedicated meter.
var failedNodes = outcome.FailedNodes;
failedNodes += _applier.MaterialiseHierarchy(composition);
// T14 — scripted alarms get their own pass right after the hierarchy so the equipment
// folders they parent under already exist. Materialises real Part 9 AlarmConditionState
// nodes (keyed by ScriptedAlarmId so AlarmStateUpdate writes target them); disabled
// alarms are skipped.
_applier.MaterialiseScriptedAlarms(composition);
failedNodes += _applier.MaterialiseScriptedAlarms(composition);
// Equipment-namespace tags get their own pass: ensures each signal's Variable (and any
// FolderPath sub-folder) exists under its already-materialised equipment folder so
// clients can browse them. Live values are pushed by DriverHostActor.ForwardToMux after
// each subscription cycle; variables show BadWaitingForInitialData only until the first
// publish interval fires.
_applier.MaterialiseEquipmentTags(composition);
failedNodes += _applier.MaterialiseEquipmentTags(composition);
// Equipment-namespace VirtualTags get their own pass right after the equipment tags:
// ensures each computed signal's Variable (and any FolderPath sub-folder) exists under its
// equipment folder with a folder-scoped NodeId. VirtualTagHostActor.OnResult pushes live
// values once the first dependency update arrives; until then variables show BadWaitingForInitialData.
_applier.MaterialiseEquipmentVirtualTags(composition);
failedNodes += _applier.MaterialiseEquipmentVirtualTags(composition);
OtOpcUaTelemetry.OpcUaSinkWrite.Add(1, new KeyValuePair<string, object?>("kind", "rebuild"));
_log.Info("OpcUaPublish: applied rebuild (correlation={Correlation}, added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
msg.Correlation, outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes, outcome.RebuildCalled);
if (outcome.RebuildFailed || failedNodes > 0)
{
OtOpcUaTelemetry.OpcUaApplyFailed.Add(1,
new KeyValuePair<string, object?>("kind", outcome.RebuildFailed ? "rebuild" : "nodes"));
_log.Error(
"OpcUaPublish: address-space apply DEGRADED (correlation={Correlation}, rebuildFailed={RebuildFailed}, failedNodes={FailedNodes}, added={Added}, removed={Removed}, changed={Changed}) — the running address space may be stale or partial",
msg.Correlation, outcome.RebuildFailed, failedNodes,
outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes);
}
else
{
_log.Info("OpcUaPublish: applied rebuild (correlation={Correlation}, added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
msg.Correlation, outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes, outcome.RebuildCalled);
}
}
catch (Exception ex)
{
@@ -412,7 +428,16 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
_log.Debug("OpcUaPublish: no applier wired — discarding MaterialiseDiscoveredNodes for {Equipment}", msg.EquipmentRootNodeId);
return;
}
_applier.MaterialiseDiscoveredNodes(msg.EquipmentRootNodeId, msg.Folders, msg.Variables);
var failedNodes = _applier.MaterialiseDiscoveredNodes(msg.EquipmentRootNodeId, msg.Folders, msg.Variables);
if (failedNodes > 0)
{
// archreview 01/S-1: a swallowed discovered-node injection failure surfaces at Error + the
// dedicated meter instead of vanishing into per-node Warnings.
OtOpcUaTelemetry.OpcUaApplyFailed.Add(1, new KeyValuePair<string, object?>("kind", "nodes"));
_log.Error(
"OpcUaPublish: discovered-node injection DEGRADED for {Equipment} (failedNodes={FailedNodes}) — some discovered nodes may be missing",
msg.EquipmentRootNodeId, failedNodes);
}
}
private void HandleServiceLevelChanged(ServiceLevelChanged msg)