Files
scadaproj/ZB.MOM.WW.Health/CLAUDE.md
T

9.5 KiB
Raw Blame History

ZB.MOM.WW.Health

Health-check libraries for the ZB.MOM.WW SCADA family (OtOpcUa, MxAccessGateway, ScadaBridge). These are libraries, not a service — each package is linked directly into the consuming application at build time. There is no central health process or network hop; probes run in-process alongside the application.

The library normalizes the three-tier health endpoint convention (/health/ready, /health/active, /healthz) and provides reusable probe implementations so the three sister projects share a common surface without duplicating probe logic.

Built at 0.3.0 and published to the Gitea NuGet feed. Adopted by all four apps — adoption is tracked in ~/Desktop/scadaproj/components/health/GAPS.md.


Packages

Package Responsibilities Key Dependencies
ZB.MOM.WW.Health Core tier convention, MapZbHealth extension, canonical JSON writer (ZbHealthWriter), IActiveNodeGate seam, GrpcDependencyHealthCheck reachability probe, tier-tag constants (ZbHealthTags). No Akka or EF dependency. Microsoft.AspNetCore.App (framework ref), Grpc.Net.Client
ZB.MOM.WW.Health.Akka AkkaClusterHealthCheck with a configurable AkkaClusterStatusPolicy (presets: Default / OtOpcUaCompat); ClusterActiveNode — the family-wide oldest-Up-member rule, optionally role-scoped; ActiveNodeHealthCheck over it (role-preference list, activeRole/activeNode in the result data); and AkkaActiveNodeGate, which backs IActiveNodeGate from the same rule. ZB.MOM.WW.Health, Akka.Cluster
ZB.MOM.WW.Health.EntityFrameworkCore DatabaseHealthCheck<TContext> — resolves IDbContextFactory<TContext> when registered, else a scoped TContext; pool-safe. Default probe: CanConnectAsync. Optional ProbeQuery delegate for query-based validation. ZB.MOM.WW.Health, Microsoft.EntityFrameworkCore

Consumer matrix

Consumer ZB.MOM.WW.Health (core) ZB.MOM.WW.Health.Akka ZB.MOM.WW.Health.EntityFrameworkCore
OtOpcUa yes yes yes
MxAccessGateway yes — —
ScadaBridge yes yes yes

MxAccessGateway consumes the core package only — it has no Akka cluster and no EF DbContext. OtOpcUa and ScadaBridge consume all three packages.


Build, test, and pack commands

# From ZB.MOM.WW.Health/

# Build
dotnet build ZB.MOM.WW.Health.slnx
dotnet build ZB.MOM.WW.Health.slnx -c Release

# Test (no external dependencies — no running Akka cluster, no database)
dotnet test ZB.MOM.WW.Health.slnx

# Pack (three .nupkg files land in artifacts/)
dotnet pack ZB.MOM.WW.Health.slnx -c Release -o ./artifacts

All three test assemblies run offline. The Akka suite forms real in-process clusters on loopback: a single-node one for the cluster-data payload, and a two-node one built so the oldest member is not the lowest-addressed one — the only condition under which age- and leader-ordering disagree, and so the only fixture that can catch a leader-based active-node regression. It binds fixed ports 19540/19541 (distinct from OtOpcUa's equivalent fixture) and carries a sanity check that the divergence actually occurred, so the assertions cannot pass for the wrong reason:

Assembly Tests
ZB.MOM.WW.Health.Tests 25
ZB.MOM.WW.Health.Akka.Tests 45
ZB.MOM.WW.Health.EntityFrameworkCore.Tests 6
Total 76

GeneratePackageOnBuild is off — pack explicitly with the command above.


Status

Built at 0.3.0 and published to the Gitea NuGet feed. Adopted by all four apps — adoption is tracked in the component backlog:

  • ~/Desktop/scadaproj/components/health/GAPS.md — adoption order, effort, and risk

Design documentation:

  • ~/Desktop/scadaproj/components/health/spec/SPEC.md — normalized three-tier target
  • ~/Desktop/scadaproj/components/health/shared-contract/ZB.MOM.WW.Health.md — proposed API (aligned to shipped code)
  • ~/Desktop/scadaproj/components/health/current-state/ — per-project current state (code-verified)

Index history (migrated from the scadaproj root CLAUDE.md)

Moved verbatim out of the scadaproj root CLAUDE.md on 2026-09-03 so it loads only when needed. This file is now the authoritative place for this history — append new entries here and keep the root index row short.

Component-table row as it read in the index

Component Status Goal Design Implementation
Health (readiness / liveness / active-node) Adopted (feed carries 0.1.0–0.3.0, current lib version 0.3.0; all 4 apps reference it + wire MapZbHealth). 0.2.0 (2026-07-24) added an optional per-entry data object to the canonical health JSON and made AkkaClusterHealthCheck publish its cluster view (leader/selfAddress/selfRoles/memberCount/unreachableCount) — additive, so pre-0.2.0 payloads still parse. 0.2.1 (2026-07-24) is a behaviour fix with a live consumer impact: the active tier now returns Unhealthy (503) on a role-member-but-not-leader node instead of Degraded, which the spec maps to 200. The old value made /health/active answer 200 on every node, so it could not distinguish active from standby over HTTP at all — and OtOpcUa's Traefik routes the admin UI by exactly this probe, so its leader-pinning had silently never worked (lmxopcua#494). 0.3.0 (2026-07-24) finished that fix and ENDED a family-wide duplication. 0.2.1 made the tier a real 503 but still selected by RoleLeader; on a rebuilt rig the two orderings diverge live (akka leader central-1, oldest admin central-2 — the singleton host), so it would have pinned Traefik to the node not hosting the work. Leadership is address-ordered (host, then port) and has no relationship to time; singleton placement is age-ordered. They agree on a freshly-formed cluster — which is why single-node and happy-path tests never caught it — and diverge permanently after any restart. Both Akka apps had already discovered this independently and each written a private replacement rather than use the shared check (ScadaBridge OldestNodeActiveHealthCheck, OtOpcUa ClusterPrimaryHealthCheck), so the package's entire active-node surface had zero consumers — not unused but avoided, twice, for the same reason. 0.3.0 promotes those two into one ClusterActiveNode primitive (oldest Up member, optional role scope, plus an ordered role-preference so a fused node answers for the role its singletons are pinned to); ActiveNodeHealthCheck and AkkaActiveNodeGate both delegate to it, and results now carry activeRole/selfAddress/activeNode in the 0.2.0 data object so a standby reports who IS active. Two further behaviour changes: a node holding none of the preferred roles is now Unhealthy, not Healthy — the old "not applicable ⇒ Healthy" is exactly what made the tier answer 200 everywhere, and the case is reachable (OtOpcUa's RoleParser admits dev-only and cluster-only nodes); and identity compares UniqueAddress, so a node restarted on the same host:port is correctly a different member. Both apps migrated and their private copies deleted — OtOpcUa (feat/health-0.3.0-active-node @ 88adec04) also routes RedundancyStateActor.SelectOldestUpMemberOfRole through the shared primitive, so the tier and the OPC UA ServiceLevel 250/240 split cannot drift; ScadaBridge (feat/site-node-health @ 5d4853a1) routes ActiveNodeEvaluator through it, unifying the S&F delivery gate, the heartbeat IsActive stamp, the inbound-API gate and the health tier on one rule. ScadaBridge keeps SitePairActiveNodeHealthCheck deliberately — it is a thin adapter over the site's own role-scoped IClusterNodeProvider (itself now backed by the shared rule), not a second copy of it. mxgw/HistorianGateway have no Akka cluster and are unaffected. Both app branches are MERGED and PUSHED (verified 2026-08-07): OtOpcUa 88adec04 is on origin/master; ScadaBridge merged via b3dc17a5 (2026-07-24) on origin/main, private-copy deletions included. Shared ZB.MOM.WW.Health lib components/health/ ZB.MOM.WW.Health/

Narrative

The health component is fully populated: a normalized spec, a shared-contract, three current-state docs, and an adoption GAPS backlog. Shared = three-tier endpoint convention (ready/active/healthz) + canonical JSON writer + IActiveNodeGate seam + GrpcDependencyHealthCheck + AkkaClusterHealthCheck + ActiveNodeHealthCheck

  • DatabaseHealthCheck<TContext>; left per-project = which probes each app registers, orchestrator wiring, and ScadaBridge's distributed health-monitoring pipeline.

The shared library is built and lives in this repo at ZB.MOM.WW.Health/ (.NET 10; 3 packages — ZB.MOM.WW.Health, ZB.MOM.WW.Health.Akka, ZB.MOM.WW.Health.EntityFrameworkCore; 64 tests (verified 2026-07-18); dotnet pack → 3 nupkgs @ 0.1.0). Adopted by all four apps — verified 2026-07-18 (each references the package and wires MapZbHealth); the previous "not yet adopted" claim was stale. Per-app probe coverage vs the spec has NOT been re-verified — see components/health/GAPS.md. Build/test from ZB.MOM.WW.Health/: dotnet test. Consumer matrix: MxAccessGateway → core only; OtOpcUa & ScadaBridge → all three packages.