fix(opcua): condition events never block under the node-manager lock; folder-event Part 3 note (#838)

#838(a): each notifier's SDK MonitoredNode2 drains a bounded channel (4096,
FullMode=Wait) and the synchronous NodeState.ReportEvent blocks when it is
full. Condition events are reported under Lock, and the delivery task takes
Lock on a verdict-cache miss (the SDK's own GetNodeMetadata, flag off too, and
#826's IsNotifierEventVisibleTo). A burst of >4096 events under one Lock hold
therefore deadlocked the node manager -- FolderEventFloodWireTests stalled at
exactly 4097 events with enforcement on AND off (so it predates #826).

ReportConditionEvent now goes through ReportEventWithoutBlocking:
ReportEventAsync completes inline while channels have room (unchanged normal
path); on overflow the report continues asynchronously holding no thread or
lock, and later reports queue behind it so events keep report order. The
deferred work holds only frozen snapshots and drains once Lock is released;
client delivery stays bounded by each item's queue.

#838(b): documented in docs/security.md that Part 3 ReceiveEvents applies to
the event's EventType and SourceNode (the SDK check, kept first), not to the
notifier; our decision is layered on top and non-condition events on a folder
item fail closed under enforcement. The reflection item stays open.
This commit is contained in:
Joseph Doherty
2026-11-11 04:06:28 -05:00
parent ffba75bb93
commit 91e28e87be
4 changed files with 268 additions and 1 deletions
+35
View File
@@ -825,6 +825,41 @@ Behaviours worth knowing before enabling it:
enforcement only while the installed generation carries that reference — a hand-built authority (tests) must
supply `tagReferencingEquipment`. Inert with the flag off.
**Which permissions an event on a folder follows — checked against Part 3 (Gitea #838).** OPC UA Part 3's
`RolePermissions` model gives events their own permission, `ReceiveEvents` (PermissionType bit 11), and the pinned
SDK (2.0.1, `CustomNodeManager2.ValidateEventReceivePermissions`, documented there against Part 3 §8.55) applies it to
**both the event's EventType and its SourceNode** — not to the notifier the item happens to sit on. The notifier's own
permission only decides whether the item may be created/attached. So the spec's intent is *the folder AND the
source*, never "an event reported through a folder inherits the folder's permissions". This server keeps that shape:
the re-implemented `ValidateEventRolePermissions` runs the SDK's type + SourceNode check **first** and our decision
only on top (both must pass). For the event kinds that reach folder items:
- **Condition events** — SourceNode is the condition. Our decision is the condition rule above (the condition, or a
folder it legitimately notifies through for the session), which in this ACL model is the deliberate
generalisation of "the source": a `Tag` grant covers a native alarm's raw node and every UNS projection of it, and
an `Equipment` grant covers the alarms it owns or references.
- **A condition's Part 9 method audit events** (Acknowledge, Comment, Shelve…) — SourceNode is the condition, so they
are resolved to it and follow exactly the condition rule (plus the SDK's audit/SignAndEncrypt guard on the
Server-object path).
- **Any other event on a folder item** (none is produced today; `GeneralModelChangeEvent` and the rejected-write
`AuditWriteUpdateEvent` go to the Server object, #588) names no live condition and is **withheld** under
enforcement — fail closed, which is stricter than the folder's permission and never laxer than Part 3.
With the flag off only the SDK's own type + SourceNode `ReceiveEvents` check applies, as before.
**Event bursts never block under the node-manager lock (Gitea #838).** Each notifier with event items has an SDK
`MonitoredNode2` whose delivery task drains a **bounded** channel (4096, `FullMode = Wait`), and the synchronous
`NodeState.ReportEvent` **blocks** while that channel is full. Condition events are reported under the node
manager's `Lock`, and the delivery task needs that same `Lock` on every per-event verdict-cache miss — in the SDK's
own `GetNodeMetadata` (so **with the flag off too**, i.e. this predates #826) and in the #826 check. A burst of more
than 4096 events under one hold of `Lock`, starting at a miss (a new item, or right after a deploy invalidated the
verdicts), therefore deadlocked the node manager: `FolderEventFloodWireTests` stalled at exactly 4097 events, flag on
and off. Condition events now go through `ReportEventWithoutBlocking`: `ReportEventAsync` completes inline while the
channels have room (the normal case, unchanged); when one is full that report continues asynchronously, holding no
thread and no lock, and later reports queue behind it so events still leave **in report order**. The deferred work
holds only frozen snapshots — bounded by the burst — and drains once `Lock` is released; what a client receives is
still bounded by its item's queue (oldest discarded). Pinned by `FolderEventFloodWireTests` (6000 events under one
`Lock` hold with publishing disabled: completes, deferred reports drain to zero, the item delivers at most its
revised `QueueSize` in increasing order ending with the last event, and the next event is delivered).
**Model-change and audit events on the Server object are gated too** (Gitea #588). The node manager's
own server-wide events — `GeneralModelChangeEvent` (NodeAdded / NodeDeleted / DataTypeChanged) and the
`AuditWriteUpdateEvent` of a rejected device write — used to go straight to `Server.ReportEvent`, where