Repository navigation
Read node attributes without taking the node monitor - #2119
Merged
kevinherron merged 2 commits intoOct 12, 2026
Merged
kevinherron merged 2 commits into
kevinherron merged 2 commits into
Conversation
Every UaNode subclass declared getAttribute synchronized. The Read service, the access checks behind it, and sampling all read attributes through that method, so concurrent readers of one node queued on its monitor. A Read of 100 Variables takes each node's monitor about five times: once for the Value and four times for the access check's NodeClass, AccessRestrictions, AccessLevel and UserAccessLevel. With 10 clients pipelining batch Reads of the same 100 nodes, the service threads spent about 1,500 thread-seconds blocked in a 5-second window. Most of that queueing followed from how long each request took: removing the lock raises that workload by about 5 to 8%, depending on whether the per-request dispatch overhead has also been removed. The attribute fields are now volatile and getAttribute is no longer synchronized, in UaNode and every subclass that overrides it. Each read still sees the value most recently set for that attribute. Writes are unchanged. setAttribute still holds the monitor while it notifies AttributeObservers, so writes from different threads and their notifications do not interleave. A concurrent reader can now see a new value before the observers have run; before, it waited for them. Reading an attribute directly while holding another lock also no longer takes the node monitor, which removes one side of the lock-order hazard that NodeManagerLockingTest guards against.
kevinherron
added this pull request to stack #2124
October 12, 2026 15:28
|
setAttribute holds the node monitor while it notifies observers, and getAttribute no longer takes it. The new test pauses an observer mid-notification and checks that a read on another thread returns the new value without waiting. With the old synchronized getter it times out.
Contributor
Author
|
@greptileai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 2 of 6 (#2118 to #2123) of a stack of server performance changes, found by profiling the Milo server in the o6-automation/opcua-benchmarks suite.
Problem
Every
UaNodesubclass declaredgetAttribute(AttributeId)synchronized. The Read service, the access checks behind it, and sampling all read attributes through that method, so concurrent readers of the same node queued on its monitor.A Read of 100 Variables takes each node's monitor about five times: once for the Value, and four times for the access check's NodeClass, AccessRestrictions, AccessLevel and UserAccessLevel. With 10 clients pipelining batch Reads of the same 100 Int32 Variables, async-profiler's lock mode measured about 1,570 thread-seconds of monitor wait on
UaVariableNodein a 5-second window: about 300 service threads blocked at any moment.Most of that queueing was a symptom of how long each request took, not the main limit on throughput. Removing the lock alone raises that workload by about 5%, and by about 8% once part 3 (#2120) has made each request cheaper (see Results). The change is still worth making: readers no longer serialize behind writers and their observers, and a direct attribute read no longer takes the node monitor while the caller may hold other locks.
Change
The attribute fields of
UaNodeand every subclass that overridesgetAttribute(UaVariableNode,UaVariableTypeNode,UaObjectNode,UaObjectTypeNode,UaMethodNode,UaDataTypeNode,UaReferenceTypeNode,UaViewNode) are nowvolatile, andgetAttributeis no longersynchronized. Each read still returns the value most recently set for that attribute.UaVariableNode's Value read still copies the storedDataValueand stamps a new server timestamp; it now reads the field once without the lock.Writes are unchanged.
setAttributestill holds the monitor while it notifiesAttributeObservers, so writes from different threads and their notifications do not interleave.The last row removes one side of the node-monitor/manager-monitor lock order that
NodeManagerLockingTestguards against. Code that synchronizes on a node itself, as some diagnostics code does around observers, still coordinates with writers. The Javadoc ofUaNode.getAttributeandsetAttributenow describes these rules, including that an observer which itself writes to the node is notified of that write before the remaining observers hear of the first one, which was already the case.Results
Requests per second, median of 6 measured runs from 3 interleaved rounds. open62541 C clients against the Milo benchmark server, default configuration, Ryzen 9 7950X with the server on 16 CPUs. Batch Reads read 100 Int32 Variables per request; async clients keep 32 requests in flight.
The effect is under concurrency. To check that it does not depend on the stack's order, part 3 (#2120) was also measured without this PR, in three rounds of three runs each, on 10-client batch Reads:
Single-client numbers move by about ±10% between runs on every build, so the single-client rows above show no effect either way.
Tests
UaNodeAttributeLockingTest(sdk-server):attributeReadDoesNotWaitForTheNodeMonitor, one case per node class, each reading an attribute that class's owngetAttributeserves, while another thread holds the node's monitor. With the oldsynchronizedgetters, all nine cases time out.valueReadDoesNotWaitForTheNodeMonitorcoversUaVariableNode's Value branch and checks the copy keeps value, status and source time and gets a server timestamp.attributeObserversRunWhileTheWriterHoldsTheNodeMonitor(control) checks that observers still run under the writer's monitor.readDuringObserverNotificationSeesTheNewValuepauses an observer duringsetAttributeand checks that a read on another thread returns the new value without waiting. It times out with the oldsynchronizedgetter.The tests use latches and bounded waits, and release the holder thread in
finally.Verification
mvn -q -pl opc-ua-sdk/sdk-server -am verify -Dtest=UaNodeAttributeLockingTest: 12 tests pass.