fix(runtime,opcuaserver): review fast-follows — event-filter throw test, loud MapKey fallback, log shape
Accumulated fast-follows from the Task 1-11 reviews. No behaviour change on the value path. - Strengthen OpcUaPublishActorBatchTests.Sink_throw_is_swallowed_and_logged. The survival assertion alone did NOT prove the catch exists: Akka's default supervision RESTARTS a throwing actor and the restart preserves the mailbox, so the follow-up message landed either way and the test passed against a build with no catch at all. It now asserts via EventFilter that the exception NEVER reaches supervision (Expect(0) on the InvalidOperationException) and that the handler's own Warning IS logged, keeping the survival assertion. Everything that must have happened goes inside the Expect(0) action — it counts matches at the end of the action, so an assertion left outside would race the actor and pass vacuously. Verified falsifiable: deleting the catch turns the test red. - MapKey(NodeId): the non-string identifier branch collapsed to a string.Empty key silently. Such a key cannot match anything the manager stored, so the caller sees an unexplained miss rather than an error. It now logs a Utils.LogWarning naming the seam. Every current call site is provably string-only, so this should never fire. - The two WireAlarmNotifiers Utils.LogInfo sites interpolated the RealmKey struct, rendering a raw struct dump. Interpolate .Id and .Realm as separate placeholders so log-grep patterns stay consistent with the file's other sites. - Stale comments: the _eventNotifierSources key is a RealmKey now, not a namespace-qualified NodeId string; and three comments still named DriverHostActor.ForwardToMux, which Task 8 replaced with ForwardBatchToMux.
This commit is contained in:
@@ -142,11 +142,24 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <summary>Build the internal map key for an already-resolved SDK <see cref="NodeId"/> (the SDK-facing
|
||||
/// seams). Equal to <see cref="MapKey(AddressSpaceRealm, string)"/> for the same node. The SDK-facing
|
||||
/// seams only ever pass nodes this manager minted (string <c>s=</c> identifiers); a non-string
|
||||
/// identifier falls back to its string form rather than throwing.</summary>
|
||||
/// identifier falls back to its string form rather than throwing — and says so loudly, because such a
|
||||
/// key cannot match anything the manager stored, so the lookup would silently miss.</summary>
|
||||
/// <param name="nodeId">The resolved SDK node id.</param>
|
||||
/// <returns>The realm-qualified map key.</returns>
|
||||
private RealmKey MapKey(NodeId nodeId) =>
|
||||
new(RealmOf(nodeId), nodeId.Identifier as string ?? nodeId.Identifier?.ToString() ?? string.Empty);
|
||||
private RealmKey MapKey(NodeId nodeId)
|
||||
{
|
||||
if (nodeId.Identifier is string s) return new RealmKey(RealmOf(nodeId), s);
|
||||
|
||||
// Unreachable by construction — every call site passes a node this manager minted with a string
|
||||
// `s=` identifier. If it ever fires, the derived key resolves to nothing and the caller sees an
|
||||
// unexplained miss, so name the seam rather than collapsing quietly.
|
||||
#pragma warning disable CS0618 // Utils.LogWarning is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry.
|
||||
Utils.LogWarning(
|
||||
"OtOpcUaNodeManager.MapKey: non-string NodeId identifier reached the map-key seam (node {0}, identifier type {1}) — the derived key will not resolve",
|
||||
nodeId, nodeId.Identifier?.GetType().Name ?? "null");
|
||||
#pragma warning restore CS0618
|
||||
return new RealmKey(RealmOf(nodeId), nodeId.Identifier?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Gets the count of variable nodes currently managed.</summary>
|
||||
public int VariableCount => _variables.Count;
|
||||
@@ -1262,7 +1275,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// ReportEvent catch below for the same pattern). Utils.LogInfo is [Obsolete] in 1.5.378 in
|
||||
// favour of an ITelemetryContext this manager doesn't wire — suppress the deprecation.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: condition {0} not materialised — skipping notifier wiring", alarmKey);
|
||||
// Interpolate the key's PARTS, not the RealmKey struct — a struct dump would render a shape
|
||||
// no log-grep pattern in this file matches.
|
||||
Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: condition {0} (realm {1}) not materialised — skipping notifier wiring",
|
||||
alarmKey.Id, alarmKey.Realm);
|
||||
#pragma warning restore CS0618
|
||||
return;
|
||||
}
|
||||
@@ -1297,8 +1313,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
// Referencing-equipment folder not (yet) materialised — skip this one (logged, never thrown).
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: notifier folder {0} for condition {1} not present — skipped",
|
||||
folderNodeId, alarmKey);
|
||||
Utils.LogInfo("OtOpcUaNodeManager.WireAlarmNotifiers: notifier folder {0} for condition {1} (realm {2}) not present — skipped",
|
||||
folderNodeId, alarmKey.Id, alarmKey.Realm);
|
||||
#pragma warning restore CS0618
|
||||
continue;
|
||||
}
|
||||
@@ -1941,8 +1957,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// A historian is wired: advertise event history on this notifier and register it as a source.
|
||||
// The equipment-folder NodeId identifier IS the equipment id IS the ReadEventsAsync sourceName.
|
||||
folder.EventNotifier = (byte)(folder.EventNotifier | EventNotifiers.HistoryRead);
|
||||
// Key by the full (namespace-qualified) NodeId string so a Raw and a UNS folder that share the
|
||||
// same bare id are distinct sources; the VALUE stays the bare id — that is the historian
|
||||
// Key by the realm-qualified RealmKey built from the NodeId so a Raw and a UNS folder that share
|
||||
// the same bare id are distinct sources; the VALUE stays the bare id — that is the historian
|
||||
// ReadEventsAsync sourceName (== the equipment id).
|
||||
var sourceName = folder.NodeId.Identifier?.ToString() ?? string.Empty;
|
||||
_eventNotifierSources[MapKey(folder.NodeId)] = sourceName;
|
||||
|
||||
@@ -507,7 +507,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
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
|
||||
// clients can browse them. Live values are pushed by DriverHostActor.ForwardBatchToMux after
|
||||
// each subscription cycle; variables show BadWaitingForInitialData only until the first
|
||||
// publish interval fires.
|
||||
failedNodes += _applier.MaterialiseEquipmentTags(composition);
|
||||
|
||||
Reference in New Issue
Block a user