Files
UA-.NETStandard/docs/PubSubHighAvailability.md
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# Description

Remove stock phrasing and correct misleading or inconsistent technical
documentation. The initial two passes covered 367 Markdown files,
followed by an interactive review of 169 editorial flags. The U5
interpretation originally deferred in that review has since received the
upstream Mantis answer recorded by #4843; remaining server/spec
follow-ups stay explicit in the CTT log.

- Correct package responsibilities, migration steps, API names,
ownership, protocol descriptions and links. Keep bundled migration
guides synchronized. Preserve historical conclusions as historical
records with separate re-check notes.
- Provide at least two rendered TOC levels throughout `docs/`. Convert
appropriate architecture figures to Mermaid without changing code
examples, transcripts or historical quotations.
- Address the October 19 feedback with concrete setup/migration
instructions, corrected certificate paths and PubSub names, shorter
repeated caveats, consistent XML comments, and a full review of the
Concepts guide. Retain #4839's API corrections.
- Integrate #4841's runtime/performance guidance and review
incoming/recent prose. Preserve dated measurements rather than claiming
new benchmark runs; clarify allocation ratios, startup-only processor
settings, JIT warm-up and the benchmark commit's scope. Missing raw
measurement artifacts and repeat ranges remain tracked by #4842.
- Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log
conflict using the updated Mantis states and OPC answers, retaining
historical framing and re-checks for C28/C32/C43.
- Address the latest naming/index feedback in `adb5bf693`: rename the
guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the
provider and DataType-declaration contracts into the conversion guide
and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and
`samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di,
FluentApi, ISA95, Lds and PubSub; update solution entries, project
references, Docker/CI paths, test fixture paths and documentation links
with exact casing.

**WoT guide scope.** This branch retains the detailed conversion guide
that #4758 had replaced with an anchor stub. Conversion limits, verified
document sets, preservation rules and provider ownership now live
together in `WotNodeSetConversion.md`; binding/runtime guidance remains
in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger
prose was shortened, the oversized argument-table cell moved into its
section, and existing anchors preserved.

**SDK policy is a separate, explicit user-directed change.**
`global.json` sets a stable minimum of `10.0.400`, `rollForward:
latestMajor`, and `allowPrerelease: false`. Newer major SDKs are
permitted; CI's explicit SDK pins are unchanged. README, GettingStarted,
DeveloperGuide and Recycling instructions reflect the possible analyzer
differences. The image-contract regression fix accepts compatible stable
SDK images instead of requiring exact equality with the floor.

The latest four rename/index threads are resolved after publication.
Three older threads remain open: universal obsolete-removal timing,
unconditional nonce-strength wording, and a blanket Vision NativeAOT
claim. Existing implementation/evidence constraints are explained in
their inline replies. The two-level TOCs remain as requested.

## Validation

- `git diff --check` and staged diff checks: passed. The untouched
upstream AutoID NodeSet retains its provenance bytes, including two
existing license-header trailing spaces.
- `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled
migration documents and manifests synchronized.
- Documentation/path validation: **101** documents retain two rendered
TOC levels (**2,228** entries), with idempotent formatting. **52** new
relative links and their anchors passed exact-case checks. No tracked
references to the old guide/sample paths remain. Both relocated
provider-contract sections are preserved in the destination.
- `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects
and the existing FluentApi/Lds/PubSub projects. Updated solution and
pump ProjectReference paths also passed exact-case existence checks.
- Latest focused commands, run from the repository root:

```powershell
# Passed: 61 channel send-order and reconnect tests.
dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal

# Passed: 5 pump datasheet tests; builds the renamed pump sample.
dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal

# Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models.
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal

# Passed: build, zero warnings/errors.
dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal

# Passed: dependent sample-test project build, zero warnings/errors.
dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal
```

Earlier focused integration checks passed 2,620 tests across AutoID,
event queues/scheduling, source generation, MCP/package profiles and
WoT. Both reviewed Mermaid diagrams rendered locally without network
access. These checks are not a full solution run.

The full solution has **not** been rerun for this review. Earlier
`dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did
not pass: three AOT projects rejected VSTest and 20 Vision tests failed
during graphics initialization with `OutOfMemoryException`. Remaining
long-running tests were stopped at the user's request. A focused Vision
rerun passed 558 tests and reproduced those 20 failures. The earlier
`dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was
blocked by the former SDK pin and was not retried after the policy
change. The user approved proceeding with those limitations documented;
current remote CI is not claimed green.

## Related Issues

- Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and
diagnostic-tool documentation refresh. #4841 supplies documentation and
earlier results, not all newly requested measurements.
- #4758 explains the conversion guide's earlier consolidation; the
current provider contracts are now consolidated into
`WotNodeSetConversion.md`.
- #4839 supplies the reviewed API corrections; #4843 supplies the
updated CTT issue record.
- The original prose cleanup and SDK policy were user-requested without
a source issue. No new runtime architecture is introduced by these
documentation edits.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:10:55 +02:00

12 KiB

PubSub High Availability

This guide explains how the OPC UA .NET Standard stack implements distributed PubSub high availability under OPC UA Part 14 §9.1.6. It complements High Availability and OPC UA Redundancy, which covers OPC 10000-4 §6.6 server, client, subscription, session, and network redundancy. For the base publisher/subscriber runtime, transports, encodings, discovery, diagnostics, and Security Key Service (SKS), see Part 14 PubSub.

PubSub redundancy is implemented by the opt-in Opc.Ua.Redundancy.PubSub library and package OPCFoundation.NetStandard.Opc.Ua.Redundancy.PubSub. A deployment that uses only the standard OPCFoundation.NetStandard.Opc.Ua.PubSub package runs with the default process-local stores and AlwaysActiveCoordinator, so every configured PubSub component is active.

Contents

Overview (as per Part 14 §9.1.6)

OPC UA Part 14 §9.1.6 defines redundant PubSub sets as active/standby deployments. Redundant publishers share the same PublisherId and identical DataSetWriter configuration, including the same WriterGroupId and DataSetWriterId. Exactly one instance is Active and drives the transport; the rest are Standby and wait to take over. Redundant subscribers use the same pattern: one active subscriber dispatches received messages to the application, while standby subscribers keep enough state to assume ownership after failover.

Part 14 defines the redundancy mode as a deployment behavior rather than a standardized PubSub information-model node. Unlike Part 4/5 server redundancy, there is no standard ServerRedundancy-style PubSub node that advertises the set. PubSub redundancy is therefore expressed through deployment configuration, shared runtime state, and protocol rules for active/standby publishers and subscribers.

Mode Standby state Stack behavior
None No standby state. The default non-redundant behavior; every instance is always active.
Cold Rebuild from shared configuration, runtime-state, and security-key stores only after failover. Lowest standby cost; failover recreates local runtime state and may restart sequence numbers.
Warm Configuration is loaded and the components are paused. Faster failover than Cold because the standby already knows the configured connections, writer groups, reader groups, writers, and readers.
Hot Warm state plus shared writer sequence checkpoints. A promoted standby restores writer sequence checkpoints with a fixed offset; continuity depends on checkpoint freshness and sequence-number wrap handling.

Broker transports also have their own high-availability mechanisms. Kafka consumer groups (Part 14 Annex B.2, used by the stack's Kafka transport) and MQTT shared subscriptions help scale and fail over subscribers at the broker layer. Those broker mechanisms complement, but do not replace, the application-level active/standby ownership described here.

How it maps onto the stack

The core PubSub package defines the seams that a redundant deployment replaces:

Seam Purpose
IPubSubActivationCoordinator Answers whether each PubSub component is PubSubComponentRole.Active or PubSubComponentRole.Standby, and raises RoleChanged when the runtime should pause or resume a component.
IPubSubLeaseStore Provides TryAcquireAsync, TryRenewAsync, and ReleaseAsync for single-owner component leases with monotonic fencing tokens.
IPubSubRuntimeStateStore Persists component PubSubState values for Cold and Warm rebuild / resume.
IPubSubSecurityKeyStore Persists SKS SecurityGroup key material so all redundant instances can use the same current and future keys.

IPubSubBuilder exposes those seams through WithActivationCoordinator, WithLeaseStore, WithRuntimeStateStore, and WithSecurityKeyStore. The distributed implementations in Opc.Ua.Redundancy.PubSub bridge them to the reusable HA infrastructure used by the server and client redundancy packages:

Distributed building block PubSub bridge
ILeaderElection LeaderElectionActivationCoordinator maps leadership to whole-instance PubSub Active / Standby roles.
ISharedKeyValueStore.CompareAndSwapAsync SharedStorePubSubLeaseStore implements fenced leases for LeaseActivationCoordinator.
ISharedKeyValueStore SharedStorePubSubRuntimeStateStore stores component runtime state and the PubSub keyspace includes Hot sequence / keep-alive checkpoints.
IRecordProtector + ISharedKeyValueStore SharedStorePubSubSecurityKeyStore encrypts and authenticates shared SKS records before writing them to the store.

There are two election paths:

  • Whole-instance active/standby. LeaderElectionActivationCoordinator wraps an ILeaderElection. When the local replica is leader, every known PubSub component is Active; when it is a follower, every component is Standby. Use this when the deployment elects one active PubSub process at a time, for example with Raft leadership or the Kubernetes Lease election from Opc.Ua.Redundancy.Kubernetes.
  • Per-component leases. SharedStorePubSubLeaseStore backs the existing LeaseActivationCoordinator. Each connection, writer group, or reader group gets its own lease key. The active owner renews the lease; standbys retry acquisition and take over when renewal stops. Compare-and-swap plus a monotonic fencing token prevents a paused or partitioned owner from resuming with stale ownership.

The low-level wiring is intentionally the same shape as the base PubSub builder:

services.AddOpcUa()
    .AddPubSub()
    .WithRuntimeStateStore(runtimeStateStore)
    .WithSecurityKeyStore(securityKeyStore)
    .WithLeaseStore(leaseStore)
    .WithActivationCoordinator(new LeaseActivationCoordinator(leaseStore, telemetry, ownerId));

The intended high-level service-collection API is AddPubSubRedundancy(Action<PubSubRedundancyOptions>?). It composes the same seams from PubSubRedundancyOptions, resolving ISharedKeyValueStore, ILeaderElection when PubSubRedundancyElection.LeaderElection is selected, and IRecordProtector for the shared SKS store.

Sequence-number continuity (Hot standby)

OPC UA Part 14 §7.2.5.4.1 defines the subscriber de-duplication key as (PublisherId, DataSetWriterId, SequenceNumber). For a redundant publisher, the shared PublisherId and identical writer configuration mean the promoted standby must continue the same logical sequence stream.

On promotion, Hot standby restores each writer to its stored sequence checkpoint + 1,000, advancing the local counter only if that value is higher. Sequence advances after the last checkpoint, store lag and wrap can still produce duplicates or gaps.

Cold and Warm may restart sequences, requiring subscribers to reset de-duplication state. Distinguish a restart from normal sequence-number wrap.

Shared security keys (SKS)

PubSub security depends on SecurityGroup keys supplied by SKS. SharedStorePubSubSecurityKeyStore implements IPubSubSecurityKeyStore on top of ISharedKeyValueStore and protects every stored SecurityGroup record with IRecordProtector. It persists the SecurityGroup id, security policy URI, key lifetime, current and future PubSubSecurityKey values, authorized caller identities, and role permissions.

The practical effect is that a promoted standby can publish, decrypt, or validate secured PubSub messages with the same current and future SecurityGroup keys as the failed active without first pulling fresh keys from SKS. If a protected record cannot be verified and decrypted, the store fails closed and does not apply it.

Networked key stores require a real authenticated-encryption protector. NullRecordProtector is only suitable for single-process demos and tests; do not use it with a networked shared store in production.

Consistency: eventual vs strong

The reusable redundancy layer exposes RedundancyConsistencyMode:

  • Eventual. Bulk replicated state uses the leaderless CRDT / gossip store, while linearizable keyspaces are routed to a strong store. This is the default model for high-throughput state that can tolerate convergence delay.
  • Strong. All shared state uses a linearizable Raft-backed ISharedKeyValueStore. Choose this when every write must be quorum-confirmed.

PubSub leases and leader election need compare-and-swap and therefore a strong keyspace. SharedStorePubSubLeaseStore uses CompareAndSwapAsync for acquisition, renewal, release, and fencing-token advancement, so it must run on a store that provides linearizable CAS for the lease/pubsub/ keyspace. Bulk runtime state, Cold / Warm component state, SKS record discovery, and Hot writer sequence checkpoints can use eventual consistency when the failover policy accepts the resulting state lag. Deployments that require no stale checkpoint reads can place those keyspaces on the strong store as well, but that alone does not establish sequence-number continuity.

Kubernetes

Use the Kubernetes high-availability package when a redundant PubSub set runs as replicas in a cluster. Opc.Ua.Redundancy.Kubernetes provides Kubernetes-Lease leader election for whole-instance active/standby deployments. One pod drives the PubSub transports; the others remain on standby.

For the full server/client deployment pattern, including role-based access control (RBAC), probes, time synchronization, peer discovery, and secret management, see Kubernetes High Availability Deployment.

Security considerations

Treat the shared store as part of the PubSub trust boundary:

  • Authenticate and encrypt the store connection.
  • Restrict network access to the redundant set.
  • Apply quotas and retention to PubSub key prefixes.
  • Monitor lease, checkpoint, runtime-state, and SKS write failures as availability signals.

Any shared secret or key material must be protected with IRecordProtector before it reaches a networked store. This is mandatory for SKS SecurityGroup records and for any future PubSub state that contains secrets. Never run production networked stores with NullRecordProtector; use a real protector such as an authenticated-encryption key-ring implementation and rotate its keys through the same operational controls used for OPC UA certificates and secrets.

Exactly one active publisher should drive a transport for a redundant writer. Validate that all redundant publishers use the same PublisherId and identical writer ids, and that standbys cannot publish while in Standby. A split-brain publisher set produces duplicate or conflicting sequence streams that subscribers can only handle as data loss or replay.

See also