Repository navigation
Let SamplingManagerConfig name the executor sampling groups run on - #2104
Merged
Merged
Conversation
Sampling groups ran every refresh and sample on the server's executor. A sampler whose reads block, such as the default AddressSpaceSamplingGroup over a slow device, held one of the server's shared threads for the whole turn. The only way to move that work was OpcUaServerConfigBuilder.setExecutor, which moves every service request along with it. SamplingManagerConfig now has an optional executor. When it is set, each group runs its turns there; when it is null, the default, groups keep using the server's executor. Timers stay on the server's scheduled executor. A group runs one turn at a time whatever the executor, so a thread-per-task executor works, including a virtual thread per turn on Java 21 or later. The caller owns the executor and the manager never shuts it down.
|
A group dispatches each turn from a scheduler task with executor.execute. A RejectedExecutionException there, from a full bounded pool or an executor already shut down, escaped into the scheduled future and was lost. A rejected cycle never scheduled the next one, so the group stopped sampling. A rejected handoff was worse: releaseTurn takes the turn before dispatching it, so the turn was never released and no cycle or initial sample could run again. A rejection is now logged and skipped. A rejected cycle schedules the next one. A rejected handoff releases the turn, and schedules the next cycle if it was a cycle's. A rejected initial sample leaves its items pending for the next cycle. The executor docs also say it must run tasks on threads of its own. An executor that runs them on the calling thread would run every turn on the server's scheduled executor.
Contributor
Author
|
@greptileai review e2ff7f3 handles a RejectedExecutionException from the sampling executor and documents that the executor must use its own threads. Replies are on each thread. |
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.
Sampling groups run every read access refresh and every
samplecall on the server's executor. When a sampler's reads block, each turn holds one of the server's shared threads until the read returns. The defaultAddressSpaceSamplingGroupover a slow device does this, and so does a driver with a synchronous protocol. With many interval groups across many address spaces, that blocking competes with service requests. The only way to move the work wasOpcUaServerConfigBuilder.setExecutor, and that moves every service request with it.This PR adds an optional executor to
SamplingManagerConfig. When it is set, each group runs its turns there. When it is null (the default), groups keep using the server's executor, so existing servers behave as before.Milo still targets Java 17. The config takes a plain
java.util.concurrent.Executor, so a server on Java 21 or later can pass a virtual-thread-per-task executor without Milo referencing virtual threads.What changes and what stays
SamplingGroupresolves its executor inconfigure, whichSamplingManagercalls before a group starts. Every path intosampleuses that executor: the timed cycle, the debounced initial sample, and a turn handed off after the previous one completed.RejectedExecutionExceptionfrom the dispatch escaped into the scheduled future and was lost. A rejected cycle never scheduled the next one. A rejected handoff never released the turn, becausereleaseTurntakes the turn before dispatching it, so the group could never sample again. Now a rejection is logged and skipped. A rejected cycle schedules the next one. A rejected handoff releases the turn, and schedules the next cycle if it was a cycle's. A rejected initial sample leaves its items for the next cycle. This applies to the server's executor too, but a bounded or closed caller-owned executor makes rejection much more likely.SamplingManagerConfiggains a seventh record component. The type is new in 1.2 and has not been released, so no released API changes.On Java 21 to 23,
synchronizedstill pins a virtual thread to its carrier (JEP 491 removed that in Java 24). The group's own locked sections are short and never block. A sampler that blocks inside its ownsynchronizedcode would still pin on those versions.Docs
The
SamplingManagerConfigandSamplingGroupJavadoc, the samplingpackage-info,docs/features/sampling.md(configuration table, a short example, and the troubleshooting row for blocked worker threads), and the 1.2.0 release notes now describe the setting.Verification
SamplingGroupTest.everyTurnRunsOnTheConfiguredExecutorconfigures an executor that marks when it is running a task. It drives an initial sample that holds the turn, a cycle that comes due meanwhile and runs on the handoff, and a cycle on time. It checks that all threesamplecalls ran inside the configured executor. It also checks that the executor received exactly four tasks: those three turns plus the cycle that found the turn held. That shows timers and the watchdog do not go through it. The existing tests cover the default path, where the executor is null and groups run on the server's executor.aRejectedCycleIsSkippedAndTheNextOneIsScheduledandaRejectedHandoffReleasesTheTurndrive an executor that rejects. They check that the next cycle is still scheduled and that the group samples again once the executor accepts. Both fail on the first commit'sSamplingGroup, where the rejection escapes the scheduler task.Run locally on e2ff7f3:
mvn -q spotless:applyandmvn -q clean compilemvn -q -pl opc-ua-sdk/sdk-server -am verifyfiltered to thesamplingpackage,ManagedAddressSpaceSamplingTest, andSubscriptionModelTest: 78 tests, 0 failures