Skip to content

Harden HA address-space hydration and replica-consistent NodeIds - #4445

Merged
marcschier merged 15 commits into
OPCFoundation:masterfrom
marcschier:fix/ha-address-space-hydration
Sep 9, 2026
Merged

Harden HA address-space hydration and replica-consistent NodeIds#4445
marcschier merged 15 commits into
OPCFoundation:masterfrom
marcschier:fix/ha-address-space-hydration

Conversation

@marcschier

@marcschier marcschier commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Description

Follow-up to #4390 for distributed address-space ownership, authoritative
hydration, and replica-consistent wire NodeIds. Tracks #4444.

Identity implementation: a78d59067 (Server policy/lifecycle/adapter seam) and
07faf5cfd (replica identity module, runtime wiring, examples and proofs).

Replica identity

  • UseReplicaNodeIdentity / ReplicaNodeIdFactory reserve fixed shared namespace
    indexes before node-manager construction and reuse DefaultNodeIdFactory.
    ApplicationUri and built-in diagnostics/configuration remain local.
  • Replicated startup requires an explicit identity contract. Namespace layout,
    deterministic mode, replica-set identity and writer policy must agree.
    Standard factory views, replacement and registration preserve the policy.
  • Active/passive writer-assigned identities are replicated unchanged. Independent
    creation requires stable keys or explicit IDs. Live IDs and retained tombstones
    are reserved before writer allocation; no network/storage call runs in New.
  • Protected store descriptors use strong routing, including customized hybrid
    prefixes. Unknown legacy state and empty eventual scans cannot authorize
    automatic adoption. Verified-new hybrid provisioning is explicit.
  • Active/active frames compare the descriptor before CRDT merge. Rejected frames
    are not reported as applied; incompatible admission remains at ServiceLevel 0
    through the standard service-level wiring.
  • Shared metadata validation covers type definitions, data types, references,
    permissions, NodeId/ExpandedNodeId/QualifiedName values, arrays/matrices,
    DataValues and encodeable structures/arguments, without reflection.
  • Runtime preparation hydrates retained IDs before replacement routes publish.
    Rebinding keeps the existing store/election or gossip transport/map and attaches
    to the new node-manager sources. Normal node-manager mutations, including
    AddNodes, reach the cached local-address-space adapter.
  • WoT materialization retains the stable configured factory. Samples expose
    genuinely factory-created nodes and a client --identity failover workflow.

Preserved HA repairs

The published d99995003 and 79da74f14 fixes remain in place:

  • Shared linearizable sequence/lease coordination; no pretend CAS on CRDT and no
    private counter substituted for shared coordination.
  • Strict authoritative record decoding, protected pending reservations, safe
    snapshot publication and required delta retention.
  • Accepted topology state controls authoritative membership; stale deletion
    cannot remove a newer incarnation.
  • Independent subtree attachment tracking and correct disposal of the final
    snapshot task.
  • Values wait for their topology, and parent tombstone horizons prevent older
    descendant values crossing incarnations.

A new deterministic regression also covers writer hydration preparation:
temporary reconciliation removals must remain inbound application, not be
recaptured as new shared deletion records.

Validation

Review feedback fixes

3a682155b addresses the active-node notification, WoT namespace-index bounds,
and retired-manager XML documentation feedback. The address-space adapter now
notifies listeners with the registered active node, and both WoT managers reject
missing/out-of-range namespace indexes before ownership is established.

Targeted feedback validation passes on net10.0 and net48: 8 address-space adapter
cases and 74 WoT cases per framework, plus 17 hydration/gossip/live-client
integration cases on net10.0. The wrong-instance notification and missing
namespace regressions were reproduced before the fixes.

Identity implementation evidence

Selected scope net10.0 net48
HA repair, identity, metadata, peer admission and real-client identity/reload fixtures 215 passed 215 passed
Factory, fluent authoring/import, NodeManagement and address-space adapter fixtures 213 passed 213 passed
WoT node-manager fixtures 68 passed 68 passed
  • Real UA-TCP clients compare full NodeIds/reference targets across independent
    servers in all four deterministic modes, with different manager/creation order
    and local allocations. Saved IDs work after endpoint failover and restart.
  • Real AddNodes-created IDs remain readable through successive live manager
    replacements in both replication modes, including nodes created after a reload.
  • Two process-level cases start three sample replicas, kill the serving process,
    verify a different serving ServerArray URI, and reuse cached factory-generated
    IDs for Read and CreateMonitoredItems without remapping or rebrowsing.
  • The identity/hydration scenario runs from the actual published win-x64 NativeAOT
    executable in the existing historian companion host.
  • The focused writer-recapture regression was observed failing without its fix,
    then passing. The real reload scenario was additionally repeated while
    diagnosing that race.
  • Changed-file formatting/style/analyzers were applied. Existing CA1508, CA1823,
    CA1861 and CA1850 warnings in untouched upstream files were not suppressed.

The selected checks above do not replace full-solution CI, coverage gates or
maintainer review, and do not claim that the entire solution or every native
platform was run. #4444 remains the tracking issue.

Contract and consistency limits

Fixed identity does not make eventual payload reads linearizable. Hybrid views
remain non-destructive, use explicit tombstones, and do not infer empty-store
bootstrap or partition completion from absence. Compacted snapshots require
authoritative storage. Failed/uncertain reservations require reconciliation.

There is no automatic namespace-slot allocation, persistent logical-key mapping
database, live renumbering, legacy-store wipe/adoption, or new hash algorithm.
Opaque shared structures require a registered codec so their namespace-bearing
fields can be validated. Application callbacks are still application-owned.

Related issues

Checklist

  • I have signed the CLA and read CONTRIBUTING.
  • Added targeted regression and integration tests.
  • Updated directly related API, HA, migration and sample documentation.
  • Ran scoped formatting and the targeted framework checks listed above.
  • Full UA.slnx matrix and coverage gates completed.
  • All CI and maintainer review completed.

marcschier and others added 10 commits September 2, 2026 13:58
Route transactional providers through their atomic update methods, preserve modified-history audit metadata, and add event plus structured-history client APIs with regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement all released Historical Access client/server facets, durable continuation takeover, truthful profile claims, complete reference workflows, and strongly consistent active/passive historian failover.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Treat ObjectDisposedException as part of best-effort HistoryRead continuation release so enumerator disposal cannot mask the original result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Preserve the latest processed-history validation and output limits while retaining continuation-aware dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Close OPCFoundation#4400 by making aggregate revision provider-aware, gating processed reads, priming aggregate monitored items from raw history, and adding tri-state Historizing control. Preserve compatibility and durable required-error delivery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Clarify continuation failover behavior, remove inapplicable migration and CTT wording, replace suppression workarounds, use Variant.From, and simplify the redundancy callback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Capture pending ownership partitioning, authoritative hydration, sequenced store, and binary-state restoration work for integration with current master. Retain master test coverage rather than the superseded NativeAOT exclusion experiment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Preserve the local ownership and authoritative-hydration work while retaining all completed historian fixes, namespace-zero exclusions, WoT reconciliation and NativeAOT partitions from master. Keep the discovered CRDT sequencing incompatibility visible as a draft blocker rather than inventing an unsafe CAS fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Address OPCFoundation#4444 and OPCFoundation#4445 with explicit strong sequence/lease routing, protected publication reservations, validated snapshot publication, and strict authoritative record decoding. Retain real CRDT polling through shared hybrid coordination without fake CAS, private replicated counters, or unsafe snapshot/log compaction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
Reconcile authoritative root membership after sequence acceptance, retain values until topology exists, and exclude pre-deletion descendant values. Track node instances independently of mutable child links and await producer-owned snapshot cleanup during disposal. Document explicit strong versus eventual hydration and recovery contracts for OPCFoundation#4445.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
marcschier and others added 3 commits September 8, 2026 17:46
Integrate master105508bcb before replica identity enforcement. Preserve WoT ownership partitions while adopting the shared factory and retaining async configure/seal and lifecycle/import ordering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
…ager changes

Add the optional factory policy seam, awaited lifecycle preparation/rebinding, and cached address-space adapters that observe normal node-manager mutations without duplicating hydration notifications.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
… live reload

Bind fixed namespace and factory contracts to protected state and gossip admission. Preserve writer/retained identities, validate namespace-bearing metadata and structures, hydrate prepared managers and rebind capture, and exercise unchanged client IDs through real process failover, AddNodes, live reload, restart, and NativeAOT.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
@marcschier marcschier changed the title Harden HA address-space ownership and hydration Harden HA address-space hydration and replica-consistent NodeIds Sep 8, 2026
@marcschier
marcschier marked this pull request as ready for review September 9, 2026 04:59
Copilot AI lite review requested due to automatic review settings September 9, 2026 04:59

Copilot AI left a comment

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.

🔵 Needs a closer look

It introduces broad, cross-cutting changes to HA replication/identity and lifecycle wiring that warrant maintainer review beyond targeted test evidence.

Pull request overview

This draft follow-up hardens redundant-server address-space replication by enforcing a replica-set NodeId identity contract (fixed shared namespace slots + policy-preserving factories) and by strengthening hydration/registration hooks so replicas keep identical wire NodeIds across failover and node-manager replacement.

Changes:

  • Add replica identity plumbing (INodeIdFactoryPolicy, ReplicaNodeIdFactory integration, ownership partitions) and validate identity/store/peer contracts early in startup and during lifecycle changes.
  • Harden runtime/hydration behavior (address-space ownership routing, in-place node updates, binary deserialization defaults for omitted attributes).
  • Add targeted regression/integration tests plus samples/docs demonstrating identity-preserving client failover.
File summaries
File Description
tests/Opc.Ua.Types.Tests/State/NodeStateTests.cs Assert binary-load defaults reset omitted attrs
tests/Opc.Ua.Types.Tests/State/MethodStateTests.cs Assert omitted method flags deserialize deterministically
tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs Startup test updated for shared-namespace identity
tests/Opc.Ua.Redundancy.Server.Tests/Raft/RedundancyConsistencyBuilderExtensionsTests.cs Strong-prefix routing regression for HA coordination
tests/Opc.Ua.Redundancy.Server.Tests/Identity/ReplicaNodeIdFactoryTests.cs New: identity contract/unit coverage
tests/Opc.Ua.Redundancy.Server.Tests/Identity/ReplicaIdentityStoreTests.cs New: protected store contract verification
tests/Opc.Ua.Redundancy.Server.Tests/Identity/ReplicaIdentityLifecycleTests.cs New: replacement hydration + writer recapture regression
tests/Opc.Ua.Redundancy.Server.Tests/Identity/ReplicaIdentityGossipTests.cs New: CRDT framing/admission identity tests
tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs Ownership/partition seeding and failure-path coverage
tests/Opc.Ua.Redundancy.Samples.Tests/TestPorts.cs Add adjacent-port selection helper for samples
tests/Opc.Ua.Redundancy.Samples.Tests/SampleHaShortHaulTests.cs Add process-level identity failover scenario
tests/Opc.Ua.Redundancy.Samples.Tests/RedundantServerCluster.cs Add active/active cluster boot helper
tests/Opc.Ua.Aot.Tests.Historian/ReplicaNodeIdentityAotTests.cs New: NativeAOT identity/hydration proof
src/Opc.Ua.WotCon.Server/WotRegistryNodeManager.cs Add explicit distributed ownership partitioning
src/Opc.Ua.WotCon.Server/WotConnectivityNodeManager.cs Add explicit ownership + policy-aware factory mode
src/Opc.Ua.WotCon.Server/WotConModelPartition.cs Expose NodeId-based partition helpers
src/Opc.Ua.Types/State/NodeState.cs Apply binary-load defaults; add UpdateFrom helper
src/Opc.Ua.Types/Opc.Ua.Types.csproj InternalsVisibleTo for redundancy server
src/Opc.Ua.Server/Server/StandardServer.cs Policy-preserving NodeIdFactory replacement + pre-start task hook
src/Opc.Ua.Server/NodeManager/PredefinedNodesAddressSpace.cs Track driving ops and add internal notify helpers
src/Opc.Ua.Server/NodeManager/Lifecycle/NodeManagerLifecycle.cs Rebind identity around commit/publication flows
src/Opc.Ua.Server/NodeManager/INodeIdFactoryPolicy.cs New: identity policy contract for factories/registration
src/Opc.Ua.Server/NodeManager/ILocalAddressSpaceSource.cs Add ILocalAddressSpaceOwnership for explicit partitions
src/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs Cache local address space; validate registration via policy; notify routing
src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs Add pre-start identity validation + shared partition routing
src/Opc.Ua.Redundancy.Server/State/ReplicaLocalAddressSpace.cs New: route shared graph across partitions
src/Opc.Ua.Redundancy.Server/State/ReplicaIdentityStore.cs New: protected identity-contract binding in store
src/Opc.Ua.Redundancy.Server/State/NodeStateSerializer.cs Add in-place update helpers + descendant NodeId collection
src/Opc.Ua.Redundancy.Server/State/INodeStateStore.cs Add internal store capability/probing interfaces
src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs Add pre-start validation and partitioned synchronizer start/stop
src/Opc.Ua.Redundancy.Server/State/AddressSpaceRegistration.cs New: validate ownership contracts + partition ids
src/Opc.Ua.Redundancy.Server/ReplicatedServerBuilderExtensions.cs Register replicated startup task as pre-start + startup
src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs Clamp service level to 0 on identity incompatibility
src/Opc.Ua.Redundancy.Server/NugetREADME.md Document identity/ownership/consistency requirements
src/Opc.Ua.Redundancy.Server/Identity/ReplicaNodeIdentityBuilderExtensions.cs New: DI wiring for replica identity factory/policy
src/Opc.Ua.Redundancy.Server/Identity/ReplicaNamespaceValidator.cs New: namespace-bearing metadata validation via encoder contract
src/Opc.Ua.Redundancy.Server/DistributedServerBuilderExtensions.cs Validate lease election config; contribute strong prefixes
samples/Redundancy/RedundantServer/README.md Document replica-consistent identity behavior
samples/Redundancy/RedundantServer/Program.cs Wire UseReplicaNodeIdentity based on HA mode
samples/Redundancy/RedundantServer/HaSampleNodeManager.cs Add factory-assigned nodes used by identity proof
samples/Redundancy/RedundantClient/README.md Document --identity scenario
samples/Redundancy/RedundantClient/Program.Identity.cs New: cached-NodeId failover scenario implementation
samples/Redundancy/RedundantClient/Program.cs Add --identity option and invoke scenario
docs/ReplicaNodeIdentity.md New: end-to-end replica identity documentation
docs/README.md Link new identity documentation
docs/NodeManagers.md Document identity requirement for replicated managers
docs/NodeIdAssignment.md Point redundant setups to UseReplicaNodeIdentity
docs/MigrationGuide.md Add migration guidance for replica-consistent NodeIds
docs/HighAvailability.md Document identity module + active/passive consistency constraints
Review details
  • Files reviewed: 56/56 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Opc.Ua.Server/NodeManager/PredefinedNodesAddressSpace.cs
Comment thread src/Opc.Ua.WotCon.Server/WotConnectivityNodeManager.cs
Comment thread src/Opc.Ua.WotCon.Server/WotRegistryNodeManager.cs Outdated
Comment thread src/Opc.Ua.Server/NodeManager/Lifecycle/NodeManagerLifecycle.cs Outdated
…ownership

Notify listeners with the node actually registered by the add pipeline, reject missing or overflowing WoT ownership namespace indexes, and document retired-manager cleanup exceptions. Add regression coverage for replacement instances and namespace bounds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
@marcschier
marcschier enabled auto-merge (squash) September 9, 2026 10:25
@marcschier
marcschier merged commit 4633aa7 into OPCFoundation:master Sep 9, 2026
264 of 265 checks passed
romanett added a commit that referenced this pull request Sep 10, 2026
Conflict in HaSampleNodeManager.CreateAddressSpaceAsync, where #4445 added
AddFactoryAssignedNodes next to the HistoryEvents object this branch had
converted to the staged builder. Both are kept: the factory-assigned
subtree stays hand-built and unstaged on purpose, because it exists to
show what an identity minted straight through NodeIdFactory looks like
beside the sample's named ones, and it is attached after the folder has
been staged so those identifiers survive untouched.

The merge also exposed a defect in this branch's New override. Naming
every node after its browse name is too broad: a browse name is unique
only among its siblings, and the historian hangs an "HA Configuration"
object off each node it historizes, so the Counter's and the
HistoryEvents' one both landed on ns=2;s=HA Configuration and the strong
active/passive replicas refused to start. The override is now limited to
the sample folder and its direct children - the nodes the redundant
client spells out - and everything else keeps the default factory's
derived identifier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
marcschier pushed a commit that referenced this pull request Sep 10, 2026
…d-rolled node managers (#4460)

Closes #4429.

PR #4428 added the node-creation surface to `INodeManagerBuilder`. This
picks up the remaining candidates that needed a base-class change first,
and reports on the two that turn out not to be candidates at all.

## Converted

**`samples/WotCon/FlatTagServer/FlatTagNodeManager.cs`** — now derives
from `FluentNodeManagerBase`. The private `CreateObject` /
`CreateVariable` / `CreateMethod` helpers are gone, along with every
`pumpNodeId + ".Operational.Measurements.MassFlow"` concatenation; nodes
are staged with `AddObject` / `AddVariable<T>` / `AddMethod` and the
hand-maintained `externalReferences[ObjectsFolder]` entry is replaced by
`CompleteConfigureAsync`.

The `EventNotifier` bits and `AddRootNotifierAsync(pump)` are kept
verbatim — the pump is the notifier an aggregating server subscribes to,
and OPC 10000-3 only delivers the supervision conditions to a client
that can reach one.

`SupervisionSignal` loses its `namespaceIndex` and `tagPath` parameters
and derives both from the supervision Object it hangs off, so the signal
follows wherever that Object was staged.

**`samples/Redundancy/RedundantServer/HaSampleNodeManager.cs`** — same
shape. The `Counter` still reaches `EnableDistributedValueParticipation`
through `INodeBuilder.Node`. The variables keep their `Organizes`
reference type, which the fluent default would otherwise have made
`HasComponent`.

**`samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs`** —
both `AddPredefinedNodeAsync` sites stage on the builder now, threaded
through `ConfigureAsync`; `DiNodeManager.CreateAddressSpaceAsync`
already calls `RegisterAuthoredNodesAsync` between `ConfigureAsync` and
the reverse-reference pass, so no new plumbing was needed. Both methods
became synchronous. The NodeIds are unchanged because the hand
`SystemContext.NodeIdFactory.New(...)` calls and
`AssignInstanceChildNodeIds` were already doing exactly what staging
does.

`CreateRepresentedComponent` is left alone: it sits on
`CreatePumpAsync`, a public runtime API, and so is out of scope for the
same reason as the DI device builders.

## NodeIds are part of two samples' contracts

The issue expected the staged path to mint through the manager's
`INodeIdFactory`, and it does — but for two of these samples the
identifiers are a published contract, not an implementation detail:

- the aggregating client's checked-in Thing Descriptions under
`samples/WotCon/AggregationClient/Documents/` spell out
`nsu=…SourceA;s=Pump1.Identification.Manufacturer`;
- `samples/Redundancy/RedundantClient/Program.cs` addresses `new
NodeId("Counter", ns)` directly.

Both managers therefore override `New` for those nodes instead of taking
the default factory's derived identifier, which satisfies the acceptance
criterion that each manager keeps its current address-space shape.
FlatTag mints the full dotted browse path; the HA sample mints the bare
browse name, but only for the sample folder and its direct children.

Two things are worth knowing for anyone writing such an override.

**It has to keep an identifier the caller already chose.**
`DefaultNodeIdFactory.New` returns `node.NodeId` unchanged when it is
non-null and in the manager's own namespace. Without that same guard,
`NodeState.Create(context, nodeId, …, assignNodeIds: true)` sets the
explicit id and then `AssignNodeIds` overwrites it from the factory —
which silently moved `…SupervisionProcessFluid.Cavitation.Alarm` to
`…SupervisionProcessFluid.CavitationAlarm` and broke five alarm tests
until the guard was added.

**It has to be narrow.** A browse name is unique only among its
siblings. The HA override originally named every node the manager
minted, and the historian hangs an `HA Configuration` object off each
node it historizes — so the Counter's and the HistoryEvents' one both
landed on `ns=2;s=HA Configuration` and the strong active/passive
replicas refused to start. It is now limited to the nodes the redundant
client actually spells out.

## Merge with master

Merged `origin/master` (`0e600c0f0`). The one conflict was in
`HaSampleNodeManager.CreateAddressSpaceAsync`, where #4445 added
`AddFactoryAssignedNodes` next to the `HistoryEvents` object this branch
had converted. Both are kept, and the factory-assigned subtree stays
hand-built and unstaged on purpose: it exists to show what an identity
minted straight through `NodeIdFactory` looks like beside the sample's
named ones. It is attached once the folder has been staged, so those
identifiers survive untouched and the subtree is still registered as
part of the folder's. Its parent's identifier is `s=HighAvailability` on
both sides, so the ids it mints are unchanged from master.

## Not candidates

**`src/Opc.Ua.PubSub.Server/PubSubNodeManager.cs`** is out for the
issue's own DI-device-builder reasoning. All three of its build paths
are reachable from runtime Method calls — `OnAddDataSetFolder` and
`OnRemoveDataSetFolder` → `RebuildConfigurationAddressSpaceAsync`,
`OnAddPushTarget` / `OnRemovePushTarget` →
`RebuildKeyPushTargetAddressSpaceAsync`, `OnGetSecurityGroup` /
`OnAddSecurityGroup` / `OnRemoveSecurityGroup` →
`RebuildSecurityGroupAddressSpaceAsync` — plus `ConfigurationChanged`.
Each removes its previous roots and rebuilds them, and `Add*` throws
`BadInvalidState` once `RegisterAuthoredNodesAsync` has run.
Independently of that, its parents (`PublishSubscribe`,
`PublishedDataSets`, `SecurityGroups`, `KeyPushTargets`) are owned by
the diagnostics node manager, so `AttachToParent` would reject them as
`BadNodeIdUnknown`.


**`src/Opc.Ua.OpenUsd.Server/Scene/UsdSceneMaterializer.Properties.cs`**
— the issue asked whether a builder is reachable at those call sites. It
is not. `MaterializeUsdStage` is a public, documented `ISystemContext`
extension (`docs/OpenUsd.md`, the package README) whose only input is
the context; it returns a detached subtree for the caller to register.
It already mints through `context.RequireNodeIdFactory()`, which is the
same factory the staged path uses, so there is nothing to gain and a
public signature to break.

## Verification

Run against the merged tree:

- `Opc.Ua.Di.Tests` — 425/425 pass (covers `PumpOpenUsdE2eTests`,
`PumpHostedReferenceTests`).
- `Opc.Ua.Redundancy.Samples.Tests` — 14/14 pass, including both
variants of master's new
`FactoryAssignedNodeIdsSurviveActiveReplicaFailureAsync` and
`StrongHistorianContinuationsSurviveActiveReplicaFailureAsync`.
- `Opc.Ua.WotCon.Samples.Tests` — 25 pass, 4 fail. The same 4 fail on a
clean baseline worktree
(`CompanionTypeDefinitionsMatchNativePumpServerAsync`,
`WotPumpInstanceMatchesNativePumpSubsetAsync`,
`RealSamplesAggregateSubscribeAndReplaceGenerationAsync`,
`RealSamplesRouteManagementAndConditionActionsToEachSourceAsync` — all
`BadConfigurationError` out of the aggregation client). Pre-existing,
unrelated to this change.
- All three sample projects build with 0 warnings and 0 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants