fix(docker-dev): give each pair node its own ApplicationUri and advertise its partner (#801)

Every docker-dev node served the compiled default ApplicationUri "urn:OtOpcUa" with a one-entry
Server.ServerArray, so the two halves of a redundant pair were indistinguishable by URI and peer
discovery returned nothing. Each node now sets OpcUa__ApplicationUri = urn:OtOpcUa:<service> (its
seeded ClusterNode.ApplicationUri) and OpcUa__PeerApplicationUris__0 = its pair partner. Verified on
the central pair: ServerArray reads [self, partner] on both nodes.

RedundancySupport still reads None by design (#717). The pair's 255/255 ServiceLevel was not a rig
config problem: it is the SDK 2.0.1 session-headroom overwrite filed separately.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 10:22:32 -05:00
parent 008f0b36b2
commit 013d02db95
2 changed files with 22 additions and 0 deletions
+1
View File
@@ -251,5 +251,6 @@ The full six-node host stack therefore needs roughly **6 GiB** of Docker Desktop
- Main: `opc.tcp://localhost:4840` (central-1), `opc.tcp://localhost:4841` (central-2)
- Site A: `opc.tcp://localhost:4842` (site-a-1), `opc.tcp://localhost:4843` (site-a-2)
- Site B: `opc.tcp://localhost:4844` (site-b-1), `opc.tcp://localhost:4845` (site-b-2)
- Each node serves its own `ApplicationUri` (`urn:OtOpcUa:<service>`, the same as its seeded `ClusterNode.ApplicationUri`) and lists its pair partner in `Server.ServerArray` (`OpcUa__ApplicationUri` / `OpcUa__PeerApplicationUris__0`). Before lmxopcua#801 every node served the compiled default `urn:OtOpcUa` with a one-entry `ServerArray`. `Server.ServerRedundancy.RedundancySupport` still reads `None` on every node, by design until #717. The healthy pair's ServiceLevel is 250 (Primary, the oldest Up `driver` member) and 240 (Secondary). Read it with `otopcua-cli redundancy -u opc.tcp://localhost:4840`.
- Galaxy + Wonderware drivers can't run in Linux containers (they need the Windows-only mxaccessgw + Historian SDK). On non-Windows, `DriverInstanceActor.ShouldStub(driverType, roles)` returns `true` for those types and the actor goes straight to a `Stubbed` state that returns deterministic success.
- SQL persistence: ConfigDb state survives container restarts (named Docker volume). Drop the volume with `down -v` for a clean slate.
+21
View File
@@ -335,6 +335,12 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "central-1"
# lmxopcua#801: each redundancy peer owns a DISTINCT ApplicationUri (docs/Redundancy.md) equal to its
# seeded ClusterNode.ApplicationUri, and advertises its pair partner in Server.ServerArray via
# PeerApplicationUris. Without these every node served the compiled default "urn:OtOpcUa" and a
# one-entry ServerArray, so a client could not tell the two halves of a pair apart.
OpcUa__ApplicationUri: "urn:OtOpcUa:central-1"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:central-2"
# Per-cluster mesh Phase 3 (config fetch-and-cache). Central SERVES the deployed-configuration
# artifact to FetchAndCache site nodes over a dedicated h2c gRPC listener (:4055), gated by the
# shared node key. Central itself is ALWAYS Direct — it IS the SQL source and must never fetch.
@@ -502,6 +508,9 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "central-2"
# lmxopcua#801: distinct ApplicationUri + pair partner in ServerArray — see central-1.
OpcUa__ApplicationUri: "urn:OtOpcUa:central-2"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:central-1"
# Per-cluster mesh Phase 3 — serves the artifact to FetchAndCache site nodes (see central-1).
# Always Direct; ConfigServe:ApiKey must match the site nodes' ConfigSource:ApiKey.
ConfigServe__GrpcListenPort: "4055"
@@ -626,6 +635,9 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "site-a-1"
# lmxopcua#801: distinct ApplicationUri + pair partner in ServerArray — see central-1.
OpcUa__ApplicationUri: "urn:OtOpcUa:site-a-1"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:site-a-2"
# FetchAndCache is MANDATORY for driver-only nodes (validator-enforced): they fetch the deployed
# artifact from central over gRPC (endpoints below) and read ONLY the LocalDb cache — no central
# SQL config read (they have no connection string). Central stays Direct + keeps ConfigServe.
@@ -746,6 +758,9 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "site-a-2"
# lmxopcua#801: distinct ApplicationUri + pair partner in ServerArray — see central-1.
OpcUa__ApplicationUri: "urn:OtOpcUa:site-a-2"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:site-a-1"
# Per-cluster mesh Phase 4: FetchAndCache mandatory for driver-only nodes (see site-a-1) — no
# ConfigDb string; fetch the artifact from central over gRPC and read only the LocalDb cache.
ConfigSource__Mode: "FetchAndCache"
@@ -848,6 +863,9 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "site-b-1"
# lmxopcua#801: distinct ApplicationUri + pair partner in ServerArray — see central-1.
OpcUa__ApplicationUri: "urn:OtOpcUa:site-b-1"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:site-b-2"
# Per-cluster mesh Phase 4: FetchAndCache mandatory for driver-only nodes (see site-a-1) — no
# ConfigDb string; fetch the artifact from central over gRPC and read only the LocalDb cache.
ConfigSource__Mode: "FetchAndCache"
@@ -921,6 +939,9 @@ services:
Cluster__Hostname: "0.0.0.0"
Cluster__Port: "4053"
Cluster__PublicHostname: "site-b-2"
# lmxopcua#801: distinct ApplicationUri + pair partner in ServerArray — see central-1.
OpcUa__ApplicationUri: "urn:OtOpcUa:site-b-2"
OpcUa__PeerApplicationUris__0: "urn:OtOpcUa:site-b-1"
# Per-cluster mesh Phase 4: FetchAndCache mandatory for driver-only nodes (see site-a-1) — no
# ConfigDb string; fetch the artifact from central over gRPC and read only the LocalDb cache.
ConfigSource__Mode: "FetchAndCache"