docs(redundancy): stop claiming RedundancySupport/ServerUriArray are exposed; record why #717 is blocked (lmxopcua#717)
v2-ci / unit-tests (push) Waiting to run

Bounded, docs-only part of #717. The standard non-transparent metadata itself is
NOT implemented here: the owner-selected contract is Warm with an explicit
degraded standby (and None standalone), today's Secondary serves Good data at
ServiceLevel 240, and the design/spec/plan for that standby behaviour is still
pending on the issue - advertising Warm now would misdescribe the server.

- docs/Redundancy.md: the "deferred pending an SDK object-type upgrade" claim
  was wrong; the pinned SDK 1.5.378.106 can build the graph
  (CreateMainNodeManagerFactory + AddBehaviourToPredefinedNode). Records what is
  and is not advertised (i=3709 SDK default, no i=11314) and the blocker.
- .claude/rules/cluster-topology.md: said each node exposes RedundancySupport
  and ServerUriArray; it does not.
- docs/OpcUaServer.md: same false claim about the SDK exposing them.

Interop gates A4/A5b stay unmet; no product code changed.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 13:12:17 -05:00
parent 8e8d4cdf13
commit 5db01053e8
3 changed files with 6 additions and 4 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ paths:
## Redundancy
The server supports non-transparent warm/hot redundancy via the `Redundancy` section in `appsettings.json`. Two instances share the same Galaxy DB and the same mxaccessgw (under distinct `MxAccess.ClientName` values) but have unique `ApplicationUri` values. Each exposes `RedundancySupport`, `ServerUriArray`, and a dynamic `ServiceLevel` based on role and runtime health. The primary advertises a higher ServiceLevel than the secondary. See `docs/Redundancy.md` for the full guide.
The server supports non-transparent warm/hot redundancy via the `Redundancy` section in `appsettings.json`. Two instances share the same Galaxy DB and the same mxaccessgw (under distinct `MxAccess.ClientName` values) but have unique `ApplicationUri` values. Each exposes a dynamic `ServiceLevel` based on role and runtime health and publishes self + peer URIs in `Server.ServerArray` (`i=2254`); the primary advertises a higher ServiceLevel than the secondary. ⚠️ It does **not** yet set `ServerRedundancy.RedundancySupport` (`i=3709`, reads the SDK default) or expose `ServerUriArray` (`i=11314`) — #717: feasible on the pinned SDK without an upgrade, but blocked on the owner-selected "Warm with a degraded standby" contract (today's standby serves Good data at 240), so do not "just set it to Warm". See `docs/Redundancy.md` for the full guide.
**Two corrections landed 2026-07-21 — both were total-outage or wrong-node defects, and both are worth knowing before touching this area.**
+1 -1
View File
@@ -67,7 +67,7 @@ Node-level authorization is backed by a permission trie under `src/Core/ZB.MOM.W
## Redundancy
`Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyStateActor` + `ServiceLevelCalculator` (`src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Redundancy/`). The OPC UA `Server/ServiceLevel` node (`VariableIds.Server_ServiceLevel`) is recomputed and republished via `SdkServiceLevelPublisher` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs`, wired as `IServiceLevelPublisher`) whenever role or driver-health changes; `ServiceLevelCalculator` produces a 0–255 value where higher means more authoritative, so the primary advertises a higher ServiceLevel than the secondary. Clients also read the standard `Server/ServerRedundancy/RedundancySupport` and `Server/ServerRedundancy/ServerUriArray` properties the SDK exposes on the ServerObject. An apply-lease prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`.
`Redundancy.Enabled = true` on the `ServerInstance` activates the `RedundancyStateActor` + `ServiceLevelCalculator` (`src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Redundancy/`). The OPC UA `Server/ServiceLevel` node (`VariableIds.Server_ServiceLevel`) is recomputed and republished via `SdkServiceLevelPublisher` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs`, wired as `IServiceLevelPublisher`) whenever role or driver-health changes; `ServiceLevelCalculator` produces a 0–255 value where higher means more authoritative, so the primary advertises a higher ServiceLevel than the secondary. Peer discovery is `Server/ServerArray` (`i=2254`, self + `PeerApplicationUris`); the standard `Server/ServerRedundancy/RedundancySupport` is not set (SDK default) and `ServerUriArray` is not exposed — see `docs/Redundancy.md` "Standard redundancy metadata" (#717). An apply-lease prevents two instances from concurrently applying a generation. See `docs/Redundancy.md`.
Peer endpoints are advertised through the standard `Server.ServerArray` property: `OpcUaApplicationHost` appends `OpcUaApplicationHostOptions.PeerApplicationUris` to `IServerInternal.ServerUris` after start so warm-redundancy clients can discover the partner.
+4 -2
View File
@@ -2,9 +2,11 @@
## Overview
OtOpcUa supports OPC UA **non-transparent** warm/hot redundancy. Two or more `OtOpcUa.Host` processes run side-by-side, share the same Config DB, and join the same Akka.NET cluster. Each process owns a distinct `ApplicationUri`; OPC UA clients discover both endpoints by reading `Server.ServerArray` (NodeId `i=2254`) on either node and pick one based on the `ServiceLevel` byte that each server publishes.
OtOpcUa runs as a 2-node **non-transparent** redundant pair (client-side failover by `ServiceLevel`; see "Standard redundancy metadata" below for what the server does and does not yet advertise). Two or more `OtOpcUa.Host` processes run side-by-side, share the same Config DB, and join the same Akka.NET cluster. Each process owns a distinct `ApplicationUri`; OPC UA clients discover both endpoints by reading `Server.ServerArray` (NodeId `i=2254`) on either node and pick one based on the `ServiceLevel` byte that each server publishes.
> **Discovery surface.** The `ServerArray` path on the `Server` object is what each node populates with self + peer `ApplicationUri`s — see `OpcUaApplicationHost.PopulateServerArray` and the per-node `PeerApplicationUris` option below. The redundancy-object-type `ServerUriArray` proper (a child of `Server.ServerRedundancy`) remains deferred pending an SDK object-type upgrade; clients should read `Server.ServerArray` for peer discovery today.
> **Discovery surface.** The `ServerArray` path on the `Server` object is what each node populates with self + peer `ApplicationUri`s — see `OpcUaApplicationHost.PopulateServerArray` and the per-node `PeerApplicationUris` option below. Clients should read `Server.ServerArray` for peer discovery today.
>
> **Standard redundancy metadata — not yet advertised (#717).** Nothing in `src/` sets `Server.ServerRedundancy.RedundancySupport` (`i=3709`), so it reads the SDK default regardless of the cluster's `RedundancyMode`, and `Server.ServerRedundancy` is the base `ServerRedundancyType` with **no** `ServerUriArray` (`i=11314`, mandatory only on `NonTransparentRedundancyType`). `ServerArray` is **not** a substitute for it. This is **not** blocked on an SDK upgrade: hermetic spikes on the pinned SDK 1.5.378.106 built the correct startup-static graph (object `i=2296` typed `NonTransparentRedundancyType` `i=2039`, `RedundancySupport`, `ServerUriArray` = effective self URI then peers) through `StandardServer.CreateMainNodeManagerFactory` + `ConfigurationNodeManager.AddBehaviourToPredefinedNode`. What blocks it is the contract: the owner-selected target is **Warm with an explicit degraded standby** (standby ServiceLevel in the degraded 2–199 band, ordinary standby tag Reads/subscriptions degraded while internal acquisition continues, standby alarm events suppressed and refreshed on promotion) and **None** for standalone deployments. Today's Secondary serves live Good data at ServiceLevel 240, so advertising `Warm` without that standby behaviour would misdescribe the server; the design, spec and plan for it are pending under #717. Interop gates A4/A5b in `docs/plans/phase-6-3-redundancy-interop-plan.md` stay unmet until then.
> **v2 change.** v1's operator-managed `ClusterNode.RedundancyRole` column + `RedundancyCoordinator` / `ApplyLeaseRegistry` / `PeerHttpProbeLoop` are gone. Primary/secondary is now derived from **Akka cluster membership** — the oldest Up member carrying the `driver` role. The operator no longer writes a role into the DB; cluster topology drives ServiceLevel automatically.