Phase 6.3 Stream C core - RedundancyStatePublisher + PeerReachability #99

Merged
dohertj2 merged 1 commits from phase-6-3-stream-c-state-publisher into v2 2026-04-19 11:33:51 -04:00
Owner

Wires Stream B pure-logic + Stream A topology loader into one orchestrator. OPC UA variable-node plumbing deferred to a narrower follow-up.

Summary

  • PeerReachability record + PeerReachabilityTracker thread-safe holder. Probe loops (Stream B.1/B.2 runtime follow-up) write; publisher reads.
  • RedundancyStatePublisher.ComputeAndPublish() reads 6 inputs (role, selfHealthy, peer HTTP/UA, apply-in-progress, recovery, topology-valid, operator maintenance) + calls ServiceLevelCalculator. Edge-triggered events: OnStateChanged on byte change, OnServerUriArrayChanged on topology content change.
  • Before-init returns NoData=1 so clients never see authoritative value from an un-bootstrapped server.
  • ServiceLevelSnapshot output record — OPC UA ServiceLevel Byte subscribes to OnStateChanged; ServerUriArray String[] subscribes to OnServerUriArrayChanged.

Test plan

  • 8 new tests: before-init → NoData; Authoritative-Primary happy path; Isolated-Primary 230 retains authority; apply dominates peer reachability (200); self-unhealthy → NoData regardless; OnStateChanged edge-triggered; OnServerUriArrayChanged fires once per content change; Standalone → 255.
  • Full solution dotnet test: 1186 passing (was 1178, +8).

🤖 Generated with Claude Code

Wires Stream B pure-logic + Stream A topology loader into one orchestrator. OPC UA variable-node plumbing deferred to a narrower follow-up. ## Summary - `PeerReachability` record + `PeerReachabilityTracker` thread-safe holder. Probe loops (Stream B.1/B.2 runtime follow-up) write; publisher reads. - `RedundancyStatePublisher.ComputeAndPublish()` reads 6 inputs (role, selfHealthy, peer HTTP/UA, apply-in-progress, recovery, topology-valid, operator maintenance) + calls ServiceLevelCalculator. Edge-triggered events: `OnStateChanged` on byte change, `OnServerUriArrayChanged` on topology content change. - Before-init returns NoData=1 so clients never see authoritative value from an un-bootstrapped server. - `ServiceLevelSnapshot` output record — OPC UA ServiceLevel Byte subscribes to OnStateChanged; ServerUriArray String[] subscribes to OnServerUriArrayChanged. ## Test plan - [x] 8 new tests: before-init → NoData; Authoritative-Primary happy path; Isolated-Primary 230 retains authority; apply dominates peer reachability (200); self-unhealthy → NoData regardless; OnStateChanged edge-triggered; OnServerUriArrayChanged fires once per content change; Standalone → 255. - [x] Full solution `dotnet test`: 1186 passing (was 1178, +8). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 added 1 commit 2026-04-19 11:33:40 -04:00
Wires the Phase 6.3 Stream B pure-logic pieces (ServiceLevelCalculator,
RecoveryStateManager, ApplyLeaseRegistry) + Stream A topology loader
(RedundancyCoordinator) into one orchestrator the runtime + OPC UA node
surface consume. The actual OPC UA variable-node plumbing (mapping
ServiceLevel Byte + ServerUriArray String[] onto the Opc.Ua.Server stack)
is narrower follow-up on top of this — the publisher emits change events
the OPC UA layer subscribes to.

Server.Redundancy additions:
- PeerReachability record + PeerReachabilityTracker — thread-safe
  per-peer-NodeId holder of the latest (HttpHealthy, UaHealthy) tuple. Probe
  loops (Stream B.1/B.2 runtime follow-up) write via Update; the publisher
  reads via Get. PeerReachability.FullyHealthy / Unknown sentinels for the
  two most-common states.
- RedundancyStatePublisher — pure orchestrator, no background timer, no OPC
  UA stack dep. ComputeAndPublish reads the 6 inputs + calls the calculator:
    * role (from coordinator.Current.SelfRole)
    * selfHealthy (caller-supplied Func<bool>)
    * peerHttpHealthy + peerUaHealthy (aggregate across all peers in
      coordinator.Current.Peers)
    * applyInProgress (ApplyLeaseRegistry.IsApplyInProgress)
    * recoveryDwellMet (RecoveryStateManager.IsDwellMet)
    * topologyValid (coordinator.IsTopologyValid)
    * operatorMaintenance (caller-supplied Func<bool>)
  Before-coordinator-init returns NoData=1 so clients never see an
  authoritative value from an un-bootstrapped server.
  OnStateChanged event fires edge-triggered when the byte changes;
  OnServerUriArrayChanged fires edge-triggered when the topology's self-first
  peer-sorted URI array content changes.
- ServiceLevelSnapshot record — per-tick output with Value + Band +
  Topology. The OPC UA layer's ServiceLevel Byte node subscribes to
  OnStateChanged; the ServerUriArray node subscribes to OnServerUriArrayChanged.

Tests (8 new RedundancyStatePublisherTests, all pass):
- Before-init returns NoData (Value=1, Band=NoData).
- Authoritative-Primary when healthy + peer fully reachable.
- Isolated-Primary (230) retains authority when peer unreachable — matches
  decision #154 non-promotion semantics.
- Mid-apply band dominates: open lease → Value=200 even with peer healthy.
- Self-unhealthy → NoData regardless of other inputs.
- OnStateChanged fires only on value transitions (edge-triggered).
- OnServerUriArrayChanged fires once per topology content change; repeat
  ticks with same topology don't re-emit.
- Standalone cluster treats healthy as AuthoritativePrimary=255.

Microsoft.EntityFrameworkCore.InMemory 10.0.0 added to Server.Tests for the
coordinator-backed publisher tests.

Full solution dotnet test: 1186 passing (was 1178, +8). Pre-existing
Client.CLI Subscribe flake unchanged.

Closes the core of release blocker #3 — the pure-logic + orchestration
layer now exists + is unit-tested. Remaining Stream C surfaces: OPC UA
ServiceLevel Byte variable wiring (binds to OnStateChanged), ServerUriArray
String[] wiring (binds to OnServerUriArrayChanged), RedundancySupport
static from RedundancyMode. Those touch the OPC UA stack directly + land
as Stream C.2 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit 8994e73a0b into v2 2026-04-19 11:33:51 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#99