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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user