[Server] Make subscription transfer between sessions transactional - #4150
[Server] Make subscription transfer between sessions transactional#4150marcschier wants to merge 16 commits into
Conversation
Transfer now prepares every subscription before any of them moves, so a failure part way through leaves the source session exactly as it was rather than with a subset of its subscriptions already gone. Monitored item resend-data triggers captured during preparation are restored when a prepared transfer is rolled back. The session publish queue tracks the transfer claim so a subscription cannot be published by the source session once it has been prepared for transfer, and cannot be lost if the transfer is abandoned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
ServerInternalData gains asynchronous disposal so the subscription manager, which now requires it, is shut down without blocking, and both dispose paths are guarded so a second call is a no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…scription-transfer # Conflicts: # src/Opc.Ua.Server/Subscription/SessionPublishQueue.cs
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Makes subscription transfer between sessions transactional and adds async disposal support to avoid blocking shutdown, with accompanying test coverage.
Changes:
- Add transfer-claim + prepared-transfer flow to prevent stale source publishing and enable rollback of monitored-item resend state.
- Introduce
IAsyncDisposableforSubscriptionManagerandServerInternalData, with idempotent dispose paths and worker shutdown coordination. - Add/enable tests covering publish-queue concurrency, transfer claims, and async/double-dispose behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Opc.Ua.Subscriptions.Tests/SessionPublishQueueConcurrencyTests.cs | New concurrency tests around publish/requeue/expiration and transfer-claim behavior. |
| tests/Opc.Ua.Server.Tests/SubscriptionTests.cs | Adds async-dispose tests and multiple transfer transaction/rollback scenarios. |
| tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs | Adds async-dispose and double-dispose coverage for ServerInternalData. |
| tests/Opc.Ua.Server.Tests/NodeManager/MasterNodeManagerDeterministicTests.cs | Re-enables rollback test previously ignored. |
| src/Opc.Ua.Server/Subscription/SubscriptionManager.cs | Implements async disposal, worker cancellation/joining, and transactional transfer orchestration. |
| src/Opc.Ua.Server/Subscription/Subscription.cs | Adds transfer reservation and prepared transfer (commit/rollback) plus transfer-in-progress guards. |
| src/Opc.Ua.Server/Subscription/SessionPublishQueue.cs | Adds transfer-claim mechanism to block stale publishing and coordinate expiration/requeue. |
| src/Opc.Ua.Server/Subscription/MonitoredItem/MonitoredItem.cs | Implements resend-trigger restoration interface for transfer rollback. |
| src/Opc.Ua.Server/Subscription/MonitoredItem/IMonitoredItem.cs | Adds internal IMonitoredItemTransferState contract for rollback. |
| src/Opc.Ua.Server/Server/ServerInternalData.cs | Adds async disposal path that attempts to dispose subscription manager asynchronously. |
| src/Opc.Ua.Server/NodeManager/MasterNodeManager.cs | Captures/restores resend trigger state during monitored-item transfer rollback. |
| src/Opc.Ua.Server/Hosting/OpcUaServerHostedService.cs | Minor typing/namespace adjustments. |
Share ServerInternalData disposal cleanup so sync and async paths leave the same observable state, while async disposal still awaits async subscription managers when available. Protect synchronous SubscriptionManager disposal with the same worker shutdown and semaphore capture used by async disposal, and make failed transfer rollback preserve unrelated destination publish requests while clearing stale source claims. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (68.69%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## marcschier/wot-05-lifecycle #4150 +/- ##
===============================================================
- Coverage 79.91% 79.89% -0.03%
===============================================================
Files 1527 1527
Lines 212839 213287 +448
Branches 36693 36766 +73
===============================================================
+ Hits 170090 170405 +315
- Misses 29983 30078 +95
- Partials 12766 12804 +38
🚀 New features to boost your workflow:
|
The transfer rollback, abandoned-claim and publish-queue requeue paths carried no tests, which is where a partial transfer would actually be observable: a failure part way through must leave the source session holding every subscription it started with. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Patch coverage: 76.96 % → 89.08 % (gate is 80 %)Measured locally by merging the Cobertura reports from
71 previously uncovered changed lines are now executed. All of them are transfer What the new tests cover13 tests were added to Publish-queue transfer claims (
Subscription-level transfer state machine (
Manager-level rollback and claim handling (
All tests are deterministic — no Test runs
The two failures are the known ones and are unrelated to this commit:
|
…scription-transfer
…scription-transfer
…scription-transfer
…o marcschier/wot-07-subscription-transfer
…o marcschier/wot-07-subscription-transfer
…o marcschier/wot-07-subscription-transfer
romanett
left a comment
There was a problem hiding this comment.
A transfer is a single method call for a client, and a nodemanager has no reason to not complete a transfer, why should the sdk implement logic for wrongly implemented node managers?
Or did i miss a benefit this brings?
Clients calling transfer multiple times?
…o marcschier/wot-07-subscription-transfer # Conflicts: # tests/Opc.Ua.Server.Tests/NodeManager/MasterNodeManagerDeterministicTests.cs
…o marcschier/wot-07-subscription-transfer
|
@romanett fair question — the machinery is not there to defend against wrongly implemented NodeManagers. Three concrete reasons, all of which are reachable with perfectly correct NodeManagers. 1. A transfer is not one call — it fans out across every owning NodeManager. A single subscription's monitored items can be spread over several NodeManagers, so 2. There are legitimate reasons for a NodeManager to refuse. It is not "no reason to not complete". A node can be deleted between the client's browse and its 3. Yes — repeated and racing transfers are real. Directly answering "clients calling transfer multiple times": a client that times out and retries produces exactly that, and two sessions can race to transfer the same subscription. The transfer claim is what makes those safe:
The tests added for the coverage gate exercise precisely these paths, all through the public/mocked seams rather than by simulating a broken NodeManager: So the benefit is: a partially-failed transfer leaves the subscription exactly where it started rather than split across two sessions, and a retry works. Happy to trim anything that is not pulling its weight — but I would not drop the prepare/rollback across multiple owners, because that failure mode does not require anyone to have implemented a NodeManager badly. |
ServerInternalData.Dispose now deliberately blocks on DisposeAsyncCore for the owner-approved sync-over-async exception, while preserving the shared disposed guard so Dispose after DisposeAsync remains a no-op. The disposal helpers were flattened into DisposeAsyncCore to keep ordering explicit around subscription manager disposal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…o marcschier/wot-07-subscription-transfer
…o marcschier/wot-07-subscription-transfer
This is PR 7 of the stacked series splitting integration PR #4093, stacked on #4147 (
marcschier/wot-05-lifecycle).This PR makes subscription transfer between sessions transactional: subscriptions are prepared before any ownership move, publish-queue transfer claims block stale source-session publishing, and rollback restores monitored-item resend-data trigger state.
ServerInternalDataalso gains asynchronous disposal so the asynchronously disposable subscription manager is shut down without blocking, with sync and async dispose paths guarded so repeated disposal is a no-op.It also brings the transfer-focused coverage for subscription transfer, publish-queue transfer claims, and
ServerInternalDataasync/double-disposal behavior.The immediate monitored-item retirement mechanism present in #4093 is deliberately excluded per maintainer decision.
IRetirableMonitoredItemandINodeManagerMonitoredItemRetirementTrackerdo not appear in this PR.