Repository navigation
Server Sampling
Collect monitored values at supported intervals and deliver them with current read permissions.
Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.
A sampling interval controls when the server obtains a MonitoredItem's value. A subscription's publishing interval controls when queued notifications are sent. Queue size, monitoring mode, deadband, and data-change filters also affect what the client sees. A short requested interval is not a promise about device latency or end-to-end delivery. Clients must use revised intervals and operation results.
Every ManagedAddressSpace owns a SamplingManager. ManagedNamespaceWithLifecycle, ManagedAddressSpaceWithLifecycle, and ManagedAddressSpaceFragmentWithLifecycle start the manager after their own startup tasks and stop it first at shutdown. A subclass of plain ManagedNamespace or ManagedAddressSpace must call getSamplingManager().startup() and shutdown() itself. Otherwise its MonitoredItems can be created with Good status and never sampled. Leave the inherited data-item callbacks in place for ordinary Variables: the default AddressSpaceSamplingGroup reads through the address space and filters, groups work by Session, splits reads at MaxNodesPerRead, and honors requested timestamps.
Override samplingManagerConfig() in a managed namespace. Import SamplingManagerConfig and ReadAccessPolicy from org.eclipse.milo.opcua.sdk.server.sampling. This example selects a 100 ms floor and cached authorization; it therefore requires application invalidation as described below.
@Override
protected SamplingManagerConfig samplingManagerConfig() {
return SamplingManagerConfig.defaults()
.withMinimumIntervalMillis(100)
.withReadAccessPolicy(ReadAccessPolicy.cached());
}The default bucket size is 25 ms. The manager rounds a requested interval upward to whole milliseconds, the minimum, and then the next bucket. Here the fixture requests 50 ms and receives a revised 100 ms, proving the configured floor took effect. A direct Read reflects a permission change at once; the MonitoredItem keeps its cached grant or denial until invalidateReadAccess(...) and the next refresh. With the unmodified defaults the minimum is 1 ms but bucketing normally makes the fastest periodic group 25 ms. Setting bucket size to zero disables bucketing.
The revised interval must be what the client is told. Do not create a second manager beside the inherited one. A Variable's MinimumSamplingInterval and server limits participate before the namespace revision hook; the default Variable value of -1 can cause a zero request to use the publishing interval first. Set a Variable's minimum deliberately when testing fastest-rate behavior.
New items use a debounced initial sample: the default delay is 100 ms and the maximum collection window is 500 ms. That bounds batching, not total first-value latency. Slow I/O or a busy worker can delay delivery. An overrun warning is a diagnostic, not cancellation or a deadline guarantee.
Override samplingGroupFactory() to return a factory that creates your SamplingGroup subclass when one device request can supply many items. onItemsChanged(List<DataItem>) gets full membership and is the place to rebuild a register plan. sample(List<DataItem>) gets the permitted items for this turn, which may be only newly added items. Permission changes do not call onItemsChanged().
This minimal group calls a synchronous readBatch adapter once for the distinct NodeIds in a turn. The adapter supplies scalar Values and must complete within a bounded time. It must return a non-null map without null values, preserving source quality and timestamps. Missing entries become Bad_NoData; an adapter failure produces Bad_CommunicationError for each item. Choose a status that describes the device condition in a production driver; the larger DeviceSamplingGroup example uses Bad_CommunicationError for a missing register value.
The inherited manager sends the group data items for every monitored attribute. This fragment does not inspect the attribute, so it incorrectly answers a non-Value item, such as DisplayName, with the device value for that NodeId. Before using it where clients can monitor other attributes, partition the items inside sample() as DeviceSamplingGroup does. Deliver device results only to Value items and read the other attributes through the address space for each Session.
static final class BatchGroup extends SamplingGroup {
private final Function<List<NodeId>, Map<NodeId, DataValue>> readBatch;
BatchGroup(
OpcUaServer server,
long intervalMillis,
Function<List<NodeId>, Map<NodeId, DataValue>> readBatch) {
super(server, intervalMillis);
this.readBatch = readBatch;
setRequestCount(1);
}
@Override
protected @Nullable CompletionStage<@Nullable Void> sample(List<DataItem> items) {
List<NodeId> ids =
items.stream().map(item -> item.getReadValueId().getNodeId()).distinct().toList();
Map<NodeId, DataValue> values;
try {
values = readBatch.apply(ids);
} catch (RuntimeException failure) {
for (DataItem item : items) {
deliver(item, new DataValue(StatusCodes.Bad_CommunicationError));
}
return null;
}
for (DataItem item : items) {
deliver(
item,
values.getOrDefault(
item.getReadValueId().getNodeId(), new DataValue(StatusCodes.Bad_NoData)));
}
return null;
}
}In the owning namespace, install the group through the factory. Here readBatch is the application-owned adapter used above:
@Override
protected SamplingGroupFactory samplingGroupFactory() {
return (server, intervalMillis) -> new BatchGroup(server, intervalMillis, readBatch);
}Imports are SamplingGroup and SamplingGroupFactory from org.eclipse.milo.opcua.sdk.server.sampling, OpcUaServer from org.eclipse.milo.opcua.sdk.server, DataItem from org.eclipse.milo.opcua.sdk.server.items, StatusCodes from org.eclipse.milo.opcua.stack.core, and NodeId/DataValue from org.eclipse.milo.opcua.stack.core.types.builtin; also java.util.List, java.util.Map, java.util.function.Function, java.util.concurrent.CompletionStage, and org.jspecify.annotations.Nullable.
The local fixture monitors three Values and checks their initial Good values, a single group with three items, and an adapter invocation containing all three distinct NodeIds. A partial response omits one NodeId: that item receives Bad_NoData with no Value, while the other two receive updated Good values. A failed device batch then produces Bad_CommunicationError with no Value on all three items. Deleting the subscription removes the group.
The DeviceSamplingGroup source example is a larger implementation with an in-memory example device. It does not establish vendor-driver interoperability. A real driver must map NodeIds and attributes to device addresses, bound requests, and return a CompletionStage that completes only after delivery or failure for the turn. Call deliver(item, value) to apply timestamp selection and item delivery. For non-Value items, the source example groups reads by Session and calls AddressSpace.read(...) with that Session's ReadContext.
The group serializes sample() and onItemsChanged(); item-added/removed hooks can overlap a sample. Multiple groups can access the same device concurrently, so the driver owns device-level serialization. A stage that never completes stalls that group. Exceptions are logged and later cycles can continue, but exceptions alone do not publish bad quality: explicitly deliver a bad DataValue for communication failure. Timeouts and cancellation belong to the driver, including at shutdown.
Direct device sampling bypasses a Variable's Value filter. Apply equivalent per-Session value transformations or use the default group where those transformations matter.
This fragment and DeviceSamplingGroup also ignore each Value item's IndexRange. The default group applies the range through the full ReadValueId. A driver serving these requests must apply it with NumericRange.parse(...) and NumericRange.readFromValueAtRange(...) from org.eclipse.milo.opcua.sdk.core, and deliver the resulting status on failure. For a Matrix, apply the range to matrix.nestedArrayValue() and wrap a multidimensional result in a new Matrix, as AttributeReader does. Otherwise an IndexRange such as "0" on a Double can incorrectly return the whole value as Good instead of Bad_IndexRangeNoData.
When IndexRange is absent, Milo's default read path still applies a requested DataEncoding to structure values, although OPC 10000-4 §7.28 says servers ignore this unused parameter. Use the address-space read path when clients rely on that Milo behavior; this fragment and DeviceSamplingGroup do not reproduce it.
The default ReadAccessPolicy.perCycle() refreshes each Session's access before collection. Cached policy stores grants and denials without expiry and needs invalidateReadAccess(...) whenever application permissions change. Invalidation does not wake the sampler or replace its stored answer immediately. Missing answers, exceptions, and AccessResult.NODE_UNKNOWN preserve the prior decision. See Access control for queue and revocation semantics.
A push-based source can own items outside the framework. Override the inherited callbacks so each item has exactly one producer. Override reviseSamplingInterval(ReadValueId, double) when its supported intervals differ; a zero interval can describe report-by-exception delivery, but the periodic SamplingManager does not implement that mode. Configure Variable/server minimum rates consistently. When an item moves from push to periodic delivery, add it with onDataItemsCreated(); modifying an item the manager never owned does not add it. Remove it with onDataItemsDeleted() when it moves the other way.
An external producer must refresh stored permissions even when no device value changes. Track creation, deletion, monitoring-mode changes, transfers, and application invalidations. SDK data-item and access-invalidation listeners are synchronous; keep them short. Session listeners are asynchronous. Snapshot the item's current Session and getAccessEpoch(), obtain decisions through the controller or managed cache, then use setReadAccessResult(result, session, accessEpoch). Also serialize or version application refresh tasks so an old task cannot overwrite a newer application decision. Refresh before re-enabled items resume delivery. Do not run a competing refresher beside framework sampling for the same items.
SubscriptionModel is a deprecated adapter over SamplingManager, with unbucketed legacy configuration. For an ordinary managed namespace, remove its field, lifecycle registration, and four forwarding callbacks; the inherited manager takes over. Preserve unrelated callback work and call super where needed. Requests may now be revised to bucket boundaries. A direct AddressSpace must explicitly own a manager, forward callbacks, start/stop it, and report the same interval revision it uses.
Sampling manager lifecycle is one-shot. Before releasing a device, stop new work, stop the manager through its owner, and cancel or settle driver requests. The framework cannot cancel an arbitrary external transport. If samples arrive twice, check for inherited plus legacy/custom producers. If they stop, inspect a manager that was never started, incomplete stages, Disabled mode, denied access, and blocked filters before changing thread counts.
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing