Skip to content

Read node attributes without taking the node monitor - #2119

Merged
kevinherron merged 2 commits into
perf/attribute-writer-primitive-arraysfrom
perf/lock-free-node-attribute-reads
Oct 12, 2026
Merged

kevinherron merged 2 commits into
perf/attribute-writer-primitive-arraysfrom
perf/lock-free-node-attribute-reads

Conversation

@kevinherron

@kevinherron kevinherron commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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 UaNode subclass declared getAttribute(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 UaVariableNode in 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 UaNode and every subclass that overrides getAttribute (UaVariableNode, UaVariableTypeNode, UaObjectNode, UaObjectTypeNode, UaMethodNode, UaDataTypeNode, UaReferenceTypeNode, UaViewNode) are now volatile, and getAttribute is no longer synchronized. Each read still returns the value most recently set for that attribute. UaVariableNode's Value read still copies the stored DataValue and stamps a new server timestamp; it now reads the field once without the lock.

Writes are unchanged. setAttribute still holds the monitor while it notifies AttributeObservers, so writes from different threads and their notifications do not interleave.

Before After
Reader while another thread writes the same node Waits for the write and all its observer callbacks Proceeds; may see the new value before the observers have run
Two writers of the same node Serialized on the monitor Unchanged
Reading an attribute directly while holding another lock Also takes the node monitor Does not take the node monitor

The last row removes one side of the node-monitor/manager-monitor lock order that NodeManagerLockingTest guards against. Code that synchronizes on a node itself, as some diagnostics code does around observers, still coordinates with writers. The Javadoc of UaNode.getAttribute and setAttribute now 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.

Workload 1.2 Before this PR With this PR Change
async, 10 clients, batch of 100 read 57,564 55,460 60,708 +9%
async, 10 clients, scalar read 540,578 540,292 543,592 +1%
sync, 1 client, batch of 100 read 6,408 6,499 6,495 0%
sync, 1 client, scalar read 34,382 31,280 32,268 +3%

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:

Build Median Reads/s
Part 1 56,000
Part 1 + this PR 59,000
Part 1 + part 3 (#2120) 102,000
Part 1 + this PR + part 3 (#2120) 110,000

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 own getAttribute serves, while another thread holds the node's monitor. With the old synchronized getters, all nine cases time out.
  • valueReadDoesNotWaitForTheNodeMonitor covers UaVariableNode'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.
  • readDuringObserverNotificationSeesTheNewValue pauses an observer during setAttribute and checks that a read on another thread returns the new value without waiting. It times out with the old synchronized getter.

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.
  • Full build with tests on the top of the stack: 5,792 tests, no failures, 4 skipped.
  • Reviewed independently by GPT-6.1-Sol (Codex) in two rounds. It searched sdk-server and integration-tests for code that relied on reads taking the monitor and found none; its two findings (the observer ordering wording and the missing Value case) are addressed.

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.
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High impact] The PR appears safe to merge; the previous test-coverage finding is addressed and no new actionable issues remain.

Summary

This PR lets node attributes be read without taking the node monitor. Writes still hold that monitor while notifying observers.

  • Node attribute reads no longer wait for the node monitor.

Reviews (2) · Last reviewed commit: "Test that a read during observer notific..." · Reviewed by Greptile

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.
@kevinherron

Copy link
Copy Markdown
Contributor Author

@greptileai review

@kevinherron
kevinherron merged commit 9da7adc into integration/1.2 Oct 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant