[Server] Decouple service-call dispatch from MasterNodeManager lifecycle coordination - #4345
Conversation
Make the class partial, wrap the retired-generation drain observer behind an internal helper, widen the notification-dispatch lease accessors and their nested types to internal, and expose the browse continuation-point limit through an internal live-read accessor. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the session-service dispatch implementations (GetManagerHandle, TranslateBrowsePaths, Browse/BrowseNext, Read, Write, Call, history access, ConditionRefresh, monitored-item create/restore/modify/ transfer/delete/set-mode, and request validation) out of MasterNodeManager into the internal NodeManagerServiceDispatcher, which depends only on the read-only routing snapshot, the server context, and the notification-dispatch lease accessors. Lifecycle mutation state stays private to MasterNodeManager and is compiler- inaccessible from the dispatcher. MasterNodeManager keeps the entire public surface unchanged in the new MasterNodeManager.ServiceDispatch.cs partial: every public virtual service method, protected helper, obsolete wrapper, and static validator retains its signature, virtual-ness, and docs, and delegates to the dispatcher. Internal dispatch calls back through the owner's virtual GetManagerHandleAsync so subclass overrides keep affecting handle resolution. Follows up #4094; part of #4098. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give the AddNodes/DeleteNodes/AddReferences/DeleteReferences service family, the NodeId-based cross-manager reference plumbing, and their dispatch/rollback helpers their own partial file with an explicit contract: this code may serialize on the dynamic-mutation semaphore (AddNodesAsync/DeleteNodesAsync) but never touches startup/shutdown or retired-generation state. MasterNodeManager.cs now contains only lifecycle coordination and shared infrastructure. Also move the IMonitoredItemTransferCoordinator explicit implementation to the ServiceDispatch partial where the rest of the service surface lives. Part of #4098. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Describe the NodeManagerServiceDispatcher split in docs/NodeManagers.md and drop the unused System.Linq using from the ServiceDispatch partial. Closes #4098. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Refactors the server-side MasterNodeManager to separate OPC UA session service-call dispatch from NodeManager lifecycle coordination by extracting dispatch logic into a dedicated internal NodeManagerServiceDispatcher, while keeping the public/virtual service surface on MasterNodeManager via new partials. This aligns with the separation requested in #4098 and reduces the size/complexity of the original monolithic class.
Changes:
- Added
NodeManagerServiceDispatcherto own routing + per-request validation for Browse/Read/Write/Call/history and monitored-item operations. - Split
MasterNodeManagerinto partials for service dispatch delegation and node-management (Add/Delete nodes & references) logic. - Updated
docs/NodeManagers.mdto document the new internal split.
Reviewed changes
Copilot reviewed 3 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Opc.Ua.Server/NodeManager/NodeManagerServiceDispatcher.cs | New internal dispatcher implementing service-call routing and permission validation. |
| src/Opc.Ua.Server/NodeManager/MasterNodeManager.ServiceDispatch.cs | New partial preserving the public/protected service surface and delegating to the dispatcher. |
| src/Opc.Ua.Server/NodeManager/MasterNodeManager.NodeManagement.cs | New partial for node-management services with mutation serialization and per-item dispatch. |
| src/Opc.Ua.Server/NodeManager/MasterNodeManager.cs | Updated to initialize and host the dispatcher + retain lifecycle coordination state. |
| docs/NodeManagers.md | Documents the new internal separation of responsibilities. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Code coverage✅ Coverage gate passed.
Uncovered changed lines
Coverage is above the recorded baseline - consider ratcheting Thresholds live in |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4345 +/- ##
==========================================
- Coverage 80.21% 80.20% -0.01%
==========================================
Files 2014 2017 +3
Lines 275050 275201 +151
Branches 47852 47855 +3
==========================================
+ Hits 220625 220738 +113
- Misses 37507 37537 +30
- Partials 16918 16926 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Description
Extracts OPC UA service-call dispatch out of
MasterNodeManagerinto a dedicated internalNodeManagerServiceDispatcher, separating it from NodeManager lifecycle coordination as agreed in the #4094 review threads (r3649968439 / r3649998212). The 7,668-line class becomes four files with explicit, documented boundary contracts:NodeManagerServiceDispatcher.cs(new, internal sealed) — the implementations of GetManagerHandle/Async, TranslateBrowsePaths, Browse/BrowseNext, Read, Write, Call, HistoryRead/HistoryUpdate, ConditionRefresh, all monitored-item operations (create/restore/modify/transfer/delete/set-mode), and per-request validation. Its constructor receives only the routing table, the server context, and a logger. Lifecycle mutation state (m_dynamicMutationSemaphore,m_startupShutdownSemaphoreSlim, retired-generation collections, preparing-manager tracking) staysprivatetoMasterNodeManager, so the dispatcher cannot reach it — the boundary is enforced by the compiler, not convention.MasterNodeManager.cs— now lifecycle coordination and shared infrastructure only: startup/shutdown, session notifications, namespace registration, allIDynamicNodeManagerHost/ISyncNodeManagerMonitoredItemRecoveryimplementations, the retired-generation notification-lease machinery, and commit/replace/rollback helpers.MasterNodeManager.ServiceDispatch.cs(new partial) — the unchanged public service surface. Every public virtual entry point, protected helper, static validator, and obsolete wrapper keeps its exact signature, virtual-ness, and XML docs, delegating to the dispatcher.MasterNodeManager.NodeManagement.cs(new partial) — the AddNodes/DeleteNodes/AddReferences/DeleteReferences service family plus NodeId-based reference plumbing. These sit between the two concerns by design: they dispatch per item but serialize address-space mutation against runtime lifecycle operations, and are the only service methods allowed to take the dynamic-mutation semaphore.The dispatcher's crossings back into
MasterNodeManagerare a small audited set: internal dispatch resolves handles through the owner's public virtualGetManagerHandleAsync(10 sites), so derived-class overrides keep affecting internal routing exactly as before; event paths consume the notification-dispatch lease accessors (5 sites, widened to internal); retired-generation drain is signalled through a new one-line internal helper instead of touching the observer field (2 sites); and the browse continuation-point limit is read live per call (2 sites).Compatibility
IMasterNodeManageris untouched; no public or protected member changes signature or losesvirtual.MasterNodeManagerWithLimitstest-framework subclass (overridesBrowseAsync, usesValidatePermissionsAsync, the per-itemBrowseAsync,UpdateDiagnostics,Server) passes unchanged.m_maxContinuationPointsPerBrowseremains a private field on the type and is read live on every call;PreHydrateMonitoredItemQueuesAsyncremains as a private delegating method.protected staticvalidators becameprotected internal static(same pattern as the existingValidateRolePermissions) so the dispatcher can call them, and the three notification-lease nested types wentprivate→internal.Reviewing tip:
git diff --color-moved=dimmed-zebrarenders the two move commits as almost entirely dimmed blocks; the non-move edits are confined to the audited call sites above and the delegation stubs. Grepping the dispatcher forIDynamicNodeManagerHost|m_dynamicMutationSemaphore|m_startupShutdownSemaphoreSlim|m_retiredGeneration|m_preparingNodeManagersmatches only its own boundary-contract doc comment.Verification
UA.slnxbuild with 0 errors;Opc.Ua.Serveritself builds with 0 warnings.Opc.Ua.Server.Testson net10.0: 4707 passed, 0 failed (5 pre-existing skips).ContinuationPointInBatch(subclass contract) 28/28.docs/NodeManagers.mddocuments the new internal split.Related Issues
Checklist
🤖 Generated with Claude Code