fix(runtime): the passive half of a LocalDb pair stands its alarm drain down for its mesh partner (review R6 MINOR-4)

The drain gate's 'is my queue-sharing peer Primary?' used only the host of
LocalDb:Replication:PeerAddress, which the passive half (ApiKey +
SyncListenPort) does not set - so as Secondary it kept draining beside its
Primary partner and every alarm event reached the historian twice.
DriverHostActor takes a sharesAlarmQueueWithLocal predicate used when no
peer host is known; WithOtOpcUaRuntimeActors builds it
(BuildAlarmQueuePeerPredicate) for a SharesRowsWithPeer node with no
PeerAddress as 'a member of my own cluster-{ClusterId} mesh other than
me'. A node sharing no rows, or with no mesh role, gets none and keeps
draining. AlarmHistorian.md / Redundancy.md / pair-replication runbook
updated.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 08:43:06 -04:00
parent e93d115cbe
commit 3be8a1217e
7 changed files with 203 additions and 11 deletions
+11
View File
@@ -103,6 +103,17 @@ Two postures are deliberate:
a key with no dial and no listener (e.g. a fleet-wide secret on an unpaired
node) captures changes but shares them with nobody, and gating it would park
its events behind a Primary that does not hold them (review R1 m3).
- **A gated node stands down only for the peer that holds its rows — and BOTH
halves of a pair know it.** The dialling half identifies its peer by the host
of `LocalDb:Replication:PeerAddress`. The passive half (`ApiKey` +
`SyncListenPort`, no `PeerAddress`) identifies it as its **per-cluster mesh
partner** (the other member of its `cluster-{ClusterId}` role). Before review
R6 MINOR-4 the passive half knew no peer at all, so as Secondary it kept
draining beside its Primary partner and the historian received every alarm
event twice. ⚠️ A passive half with **no** cluster-scoped role (a legacy,
pre-Phase-6 node) still cannot identify its partner and keeps draining — it
cannot prove which Primary holds its rows, and deferring to the wrong one
would strand them.
- **A gate that throws means "not now", never permission.** Draining on a gate
fault would put a pair into dual delivery exactly when its redundancy signal is
sick.