Skip to content

Node manager refresh: Monitored item retirement support - #4173

Draft
marcschier wants to merge 4 commits into
marcschier/wot-16-samplesfrom
marcschier/wot-17-retirement
Draft

Node manager refresh: Monitored item retirement support#4173
marcschier wants to merge 4 commits into
marcschier/wot-16-samplesfrom
marcschier/wot-17-retirement

Conversation

@marcschier

Copy link
Copy Markdown
Collaborator

Summary

  • Restores the monitored item/subscription retirement tracker dropped during the PR split.
  • Ports immediate-reload retirement onto the rewritten NodeManager lifecycle instead of reverting lifecycle files.
  • Retires old-generation monitored items with BadNodeIdUnknown and detaches retired owners after request drain; durable subscriptions keep the reference behavior and reject immediate retirement.

Validation

  • dotnet build src\Opc.Ua.Server\Opc.Ua.Server.csproj -c Release -v:q
  • dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c Release -p:CustomTestTarget=net10.0 -v:q

Results: 0 failed / 4048 passed / 8 skipped.

Restore the bespoke monitored item and subscription retirement mechanism that was dropped when the original WoT binding draft was split into stacked PRs.

Port the behavior onto the rewritten NodeManager lifecycle code instead of reverting those files, so immediate reloads retire old-generation monitored items with BadNodeIdUnknown while still disposing the retired NodeManager generation.

Co-authored-by: Copilot <[email protected]>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
@marcschier marcschier changed the title Restore monitored item retirement support Node manager refresh: Monitored item retirement support Aug 3, 2026
{
get
{
if (m_retirementNotificationPending)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldnt this leak Monitored items?
(although in a half functional state?

also the RetirementError property is imo not needed and Retire could then be simplified without a parameter.

also i dont like that this adds another check that is done in many places (error prone) and not backed by any opc ua spec (so only skd authors will eventually know about it).

No one can write to that item, so better to just queue the value and remove that checks?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - you were right, and on all four points I ended up removing the mechanism rather than defending it. Per point:

3. The check duplicated in many places (your strongest point - fixing it dissolved the others). The stack already has this exact null-object pattern: Detach parks a monitored item on the long-lived CoreNodeManager and every service path already handles that through its IsDetached branch. Retirement was reinventing it the hard way - Retire() nulled NodeManager/ManagerHandle via DetachOwner(), which is the only reason MasterNodeManager and Subscription needed the extra guard (GetRetirementError at ModifyMonitoredItems, DeleteMonitoredItems and SetMonitoringMode, plus IsRetired twice in Subscription). I deleted GetRetirementError and every one of those call sites. Retire() now parks the item on CoreNodeManager exactly like Detach, so retired items flow through the existing IsDetached routing and produce the right result with no retirement-specific check anywhere - a service added to MasterNodeManager later is covered without anyone knowing "retirement" exists.

It is also spec-backed now, which answers "only SDK authors will know": a retired item is indistinguishable from a monitored item whose Node was deleted. OPC UA Part 4 (OPC 10000-4) v1.05.07 section 5.8.4.1 (DeleteNodes) requires the server to send a Bad_NodeIdUnknown notification to monitoring clients when a monitored Node is deleted, with later service calls returning the same status. So the client sees standard behaviour, not a private convention.

2. RetirementError not needed, Retire could drop the parameter. Both gone. IRetirableMonitoredItem is now just void Retire(). The error was never variable - the single caller always passed new ServiceResult(StatusCodes.BadNodeIdUnknown) - so I dropped the parameter, the RetirementError property and the separate DetachOwner() member. In NodeManagerLifecycle the ServiceResult? immediateRetirementError plumbing becomes a plain bool immediateRetirement.

4. No one can write to it, so just queue the value and remove the checks. That is exactly the mechanism now. Retire() disposes the datachange queue handler, sets the terminal Bad_NodeIdUnknown as m_lastValue and notifies the subscription; the value is queued and no writer can reach the item because its owner is the tombstone CoreNodeManager. The IsReadyToPublish short-circuit at line 437 that you anchored on is deleted - publishing goes through the normal ready path.

1. Would this leak monitored items? No - bounded, and now provably so. A retired item stays in the subscription with its terminal value only until the client calls DeleteMonitoredItems (returns Good) or the subscription/session is torn down: the same lifetime as any monitored item on a deleted Node. It is bounded by the items the client created; the server never grows it. The contract test NodeManagerLifecycleTests.ImmediateReloadAsyncReportsBadNodeIdUnknownAndDisposesPriorGenerationAsync walks the whole path - publish returns BadNodeIdUnknown, Modify returns BadNodeIdUnknown, SetMonitoringMode returns Good, and after DeleteMonitoredItems it asserts subscription.MonitoredItemCount is zero. Because Retire() is idempotent (it returns early once parked), the lifecycle's old two-phase retire-then-detach cleanup collapsed into a single call.

Net: 153 insertions, 402 deletions. Opc.Ua.Server.Tests is green (4040 passed, 0 failed) and the multi-TFM Release build is 0 warnings / 0 errors. I have left the thread open - whether this settles it is your call.

Addressing romanett's review feedback on PR #4173, this removes the bespoke
"retirement" mechanism for monitored items owned by a NodeManager generation
that is retired during an immediate shadow reload, and instead reuses the
existing detach-to-CoreNodeManager machinery.

Previously a retired item nulled out its owner via DetachOwner(), which forced
MasterNodeManager and Subscription to special-case it: an IsRetired /
GetRetirementError predicate was re-tested at roughly eight service call sites,
and any future ownership-sensitive service had to remember to add the same
check or risk a null reference on NodeManager. That is error prone and is not
backed by the OPC UA specification, so only SDK authors would ever know about it.

Retirement is not a distinct concept: a retired item is simply a detached and
deleted item. Retire() now parks the item on the long lived CoreNodeManager (the
same null-object owner already used by Detach), marks it deleted, and queues a
terminal Bad_NodeIdUnknown, exactly like a monitored Node that was deleted (OPC
UA Part 4 section 5.8.4.1). Ownership-sensitive services (modify, delete, set
monitoring mode, transfer) then route to the parked owner and are handled by the
pre-existing IsDetached branches, so no retirement-specific checks remain. The
call is idempotent, which lets the lifecycle collapse its former two-phase
retire-then-detach cleanup into a single call.

This drops the IsRetired and RetirementError members and the Retire(error)
parameter from IRetirableMonitoredItem, the GetRetirementError helper and its
three call sites in MasterNodeManager, the IsRetired helper and
DetachRetiredMonitoredItems from Subscription, and the ImmediateRetirementError
plumbing in NodeManagerLifecycle (now a bool). The client-observable behaviour is
unchanged and remains covered by the NodeManagerLifecycleTests contract test.

Co-authored-by: Copilot <[email protected]>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
/// <summary>
/// Invalidates monitored items owned by a NodeManager before an immediate generation retirement.
/// </summary>
internal interface INodeManagerMonitoredItemRetirementTracker

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be renamed to indicate a subscription implements this functionality

ParkOnDetachedOwner(GetDetachedOwner(m_server));
m_isDeleted = true;

if ((MonitoredItemType & MonitoredItemTypeMask.DataChange) != 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this "semi dispose" is not needed and should be removed imo, just queue the value and thats it.
Also trigger for ItemReadyToPublish is not needed, as the value reports true as soon as the BadNodeID unknown is queued

return m_monitoredItems.Values
.Where(monitoredItem => IsOwnedBy(monitoredItem.Value, nodeManager))
.All(monitoredItem =>
!monitoredItem.Value.IsDurable &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the subscription is not durable monitored items can also not be durable

.Where(monitoredItem => IsOwnedBy(monitoredItem, nodeManager))
];
if (candidates.Any(monitoredItem =>
monitoredItem.IsDurable ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the subscription is not durable monitored items can also not be durable

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.

2 participants