7482a88888
#838 review of 91e28e87:
1. Ghost events. The deferred report ran the SDK walk off Lock with no
liveness check, so an event waiting behind a full channel when a deploy
removed its condition still reached i=2253 subscribers after the NodeDeleted
(a ghost Retain alarm for ScadaBridge) and folder items. Delivery now walks
the SDK path itself (OnReportEvent, OnReportEventAsync, inverse notifiers,
then the Parent as BaseInstanceState does) re-checking before EVERY hop that
the condition is the live instance; OnConditionReportEvent checks the same,
which also stops a non-live scripted condition being admitted through its
old equipment folder (#808). A guard test pins that only NodeState and
BaseInstanceState define the walk. New wire test removes the condition in
the same Lock hold as a flood; the flag-off variant fails without the checks.
2. Back-pressure. Deferral is per condition (one condition's events stay
ordered; others are not held up). Overflow policy: coalesce, newest wins --
past 1024 waiting per condition or 65,536 total, the oldest not-yet-started
event is discarded (a condition event carries full state). Warning on
entering deferral (<=1/min), Information on draining, meters
otopcua.opcua.condition_events.deferred / .discarded{reason}.
3. Verified by decompiling SDK 2.0.1: the notifier list is snapshotted under the
node's own lock, MonitoredNode2.Dispose completes its writer (pending writes
fail with ChannelClosedException, swallowed), and the consumer catches
per-event exceptions -- no fix needed; documented.
4. ConditionRefresh does not flush deferrals; documented as a residual.
#844 review: DescribeCredential canonicalises the key id (trim, lowercase;
non-hex ids fold into one identity) and the username, since ConfigDbApiKeyStore
matches with SQL Server's case-insensitive, trailing-space-blind `=`.
42 lines
1.9 KiB
C#
42 lines
1.9 KiB
C#
using System.Reflection;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #838 review: <c>OtOpcUaNodeManager.DeliverConditionEventAsync</c> re-implements the SDK's event walk
|
|
/// (<c>NodeState.ReportEventAsync</c>, plus <c>BaseInstanceState</c>'s forward to its Parent) so it can re-check the
|
|
/// condition's liveness before every hop. That copy is only faithful while no other node type changes the walk —
|
|
/// missing the Parent hop alone silently stopped condition events reaching their folders. This pins the set of
|
|
/// types that define <c>ReportEvent</c> / <c>ReportEventAsync</c> across the SDK and this server.
|
|
/// </summary>
|
|
public sealed class ConditionEventWalkGuardTests
|
|
{
|
|
[Fact]
|
|
public void Only_NodeState_and_BaseInstanceState_define_the_event_walk()
|
|
{
|
|
Assembly[] assemblies =
|
|
[
|
|
typeof(NodeState).Assembly, // Opc.Ua.Types
|
|
typeof(ApplicationConfiguration).Assembly, // Opc.Ua.Core
|
|
typeof(StandardServer).Assembly, // Opc.Ua.Server
|
|
typeof(OtOpcUaNodeManager).Assembly, // this server
|
|
];
|
|
|
|
var definers = assemblies.Distinct()
|
|
.SelectMany(a => a.GetTypes())
|
|
.Where(t => typeof(NodeState).IsAssignableFrom(t))
|
|
.Where(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)
|
|
.Any(m => m.Name is nameof(NodeState.ReportEvent) or nameof(NodeState.ReportEventAsync)))
|
|
.Select(t => t.FullName)
|
|
.Order()
|
|
.ToArray();
|
|
|
|
definers.ShouldBe(["Opc.Ua.BaseInstanceState", "Opc.Ua.NodeState"],
|
|
"a new override changes the event walk — mirror it in OtOpcUaNodeManager.DeliverConditionEventAsync");
|
|
}
|
|
}
|