fix(r2-04): Safe* helpers report failure; Apply tallies removal-pass failures into FailedNodes

This commit is contained in:
Joseph Doherty
2026-07-13 10:38:10 -04:00
parent 437f82b145
commit b3d57ca363
3 changed files with 61 additions and 16 deletions
@@ -82,14 +82,17 @@ public sealed class AddressSpaceApplier
var ts = DateTime.UtcNow;
var removedCount = 0;
// Swallowed removal-pass condition-write failures are tallied here (not lost to a per-node Warning)
// so a degraded removal is operator-visible via AddressSpaceApplyOutcome.FailedNodes (archreview 01/S-1).
var failedNodes = 0;
foreach (var eq in plan.RemovedEquipment)
{
SafeWriteAlarmCondition(eq.EquipmentId, RemovedConditionState, ts);
if (!SafeWriteAlarmCondition(eq.EquipmentId, RemovedConditionState, ts)) failedNodes++;
removedCount++;
}
foreach (var alarm in plan.RemovedAlarms)
{
SafeWriteAlarmCondition(alarm.ScriptedAlarmId, RemovedConditionState, ts);
if (!SafeWriteAlarmCondition(alarm.ScriptedAlarmId, RemovedConditionState, ts)) failedNodes++;
removedCount++;
}
// Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write
@@ -213,7 +216,7 @@ public sealed class AddressSpaceApplier
// currently-historized set. Same non-blocking + throw-safe discipline as the provisioning hook.
FeedHistorizedRefs(plan);
return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt, rebuildFailed);
return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt, rebuildFailed, failedNodes);
}
/// <summary>
@@ -617,16 +620,24 @@ public sealed class AddressSpaceApplier
.Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count());
}
private void SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName)
/// <summary>Ensure a folder node, swallowing (and Warning-logging) any sink fault. Returns <c>true</c>
/// on success, <c>false</c> when the sink threw — callers tally the false into their pass's failed-node
/// count so a swallowed materialise failure is operator-visible (archreview 01/S-1).</summary>
/// <returns><c>true</c> when the folder was ensured; <c>false</c> when the sink threw.</returns>
private bool SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName)
{
try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureFolder threw for {Node}", nodeId); }
try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); return true; }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureFolder threw for {Node}", nodeId); return false; }
}
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
/// <summary>Ensure a variable node, swallowing (and Warning-logging) any sink fault. Returns <c>true</c>
/// on success, <c>false</c> when the sink threw — callers tally the false into their pass's failed-node
/// count (archreview 01/S-1).</summary>
/// <returns><c>true</c> when the variable was ensured; <c>false</c> when the sink threw.</returns>
private bool SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
{
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureVariable threw for {Node}", nodeId); }
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); return true; }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureVariable threw for {Node}", nodeId); return false; }
}
// A VirtualTag's materialised OPC UA node (MaterialiseEquipmentVirtualTags) is derived ONLY from
@@ -682,16 +693,24 @@ public sealed class AddressSpaceApplier
Severity: 0,
Message: string.Empty);
private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts)
/// <summary>Write an alarm-condition snapshot, swallowing (and Warning-logging) any sink fault.
/// Returns <c>true</c> on success, <c>false</c> when the sink threw — the removal pass tallies the
/// false into <see cref="AddressSpaceApplyOutcome.FailedNodes"/> (archreview 01/S-1).</summary>
/// <returns><c>true</c> when the write landed; <c>false</c> when the sink threw.</returns>
private bool SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts)
{
try { _sink.WriteAlarmCondition(nodeId, state, ts); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: WriteAlarmCondition threw for {Node}", nodeId); }
try { _sink.WriteAlarmCondition(nodeId, state, ts); return true; }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: WriteAlarmCondition threw for {Node}", nodeId); return false; }
}
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
/// <summary>Materialise an alarm-condition node, swallowing (and Warning-logging) any sink fault.
/// Returns <c>true</c> on success, <c>false</c> when the sink threw — callers tally the false into
/// their pass's failed-node count (archreview 01/S-1).</summary>
/// <returns><c>true</c> when the condition was materialised; <c>false</c> when the sink threw.</returns>
private bool SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
{
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); return true; }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); return false; }
}
}