Promotes the two apps' private replacements for ActiveNodeHealthCheck into one shared primitive, and retires the leader/RoleLeader selection they were written to avoid. Both Akka consumers had already hand-rolled a replacement rather than use this package (ScadaBridge OldestNodeActiveHealthCheck, OtOpcUa ClusterPrimaryHealthCheck), so the entire active-node surface here — ActiveNodeHealthCheck, AkkaActiveNodeGate — had ZERO consumers. It was not merely unused: it was avoided, twice, for the same reason. Leadership is address-ordered (host, then port) and has no relationship to time; singleton placement is age-ordered. The two agree on a freshly-formed cluster, which is why single-node and happy-path tests never caught it. They diverge permanently after any restart: the restarted node rejoins as the youngest but keeps its address, so if it holds the lower address it becomes leader while the singletons — and all the work they own — stay on the other node. New ClusterActiveNode is the single implementation: oldest Up member, optional role scope, plus role-preference resolution for a fused node that must answer for the role its singletons are pinned to. ActiveNodeHealthCheck and AkkaActiveNodeGate both delegate to it, so an endpoint gate and the /health/active probe an orchestrator routes by cannot disagree. BREAKING (behaviour, not signature): - Selection is by age, not leadership. - The role-filtered mode no longer reports Healthy for a node LACKING the role. That "not applicable => Healthy" made the tier answer 200 on every node and silently broke leader-pinning (lmxopcua#494); it is now Unhealthy by default, overridable via NoActiveRoleStatus. This case is reachable, not defensive — OtOpcUa's RoleParser admits dev-only and cluster-role-only nodes. - Identity compares UniqueAddress, so a node restarted on the same host:port is correctly a different member during the overlap. Results now carry activeRole/selfAddress/activeNode in the 0.2.0 per-entry data object, so a standby reports WHO is active and a dashboard can render a pair from either node's payload alone. Tests: 45 Akka (was 39), 76 total. The age-vs-address divergence is pinned against a real two-node cluster built so the oldest member is not the lowest-addressed one, with a fixture sanity check so it cannot pass for the wrong reason.
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.
Packages
| Package | Description | Key Dependencies |
|---|---|---|
ZB.MOM.WW.Health |
Core tiers, MapZbHealth extension, canonical JSON writer (ZbHealthWriter), IActiveNodeGate seam, GrpcDependencyHealthCheck reachability probe, and 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 three-way / OtOpcUaCompat two-way), ActiveNodeHealthCheck with an optional role filter, and AkkaActiveNodeGate that backs IActiveNodeGate from the cluster member state. |
ZB.MOM.WW.Health, Akka.Cluster |
ZB.MOM.WW.Health.EntityFrameworkCore |
DatabaseHealthCheck<TContext> with CanConnectAsync by default and an optional ProbeQuery delegate for custom connectivity 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 (+ GrpcDependencyHealthCheck for the MxAccessGateway channel) |
yes | yes |
| MxAccessGateway | yes (+ GrpcDependencyHealthCheck for the x86 worker IPC) |
— | — |
| 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.
Versioning
All three packages are versioned lockstep from Directory.Build.props. The current release is 0.2.0. A single version bump in Directory.Build.props bumps all three packages simultaneously — consumers should reference the same version for all ZB.MOM.WW.Health packages.
0.2.0 — per-entry data (additive, non-breaking)
ZbHealthWriteremits an optional"data": { … }object per entry, sourced from the check'sHealthCheckResult.Data, only when non-empty — a check that publishes no data produces a byte-identical body to 0.1.0, so every existing consumer is unaffected. Data keys are written verbatim (the camelCase policy applies to the envelope, not to dictionary keys).AkkaClusterHealthCheckpopulates that dictionary with this node's cluster view:leader(omitted while unknown),selfAddress,selfRoles,memberCount,unreachableCount. The startup-safety paths (noActorSystem, cluster not yet accessible) stay description-only.- Consumers gain the leader field on a package bump alone — no application code change, provided the
app registers the shared
AkkaClusterHealthCheck.
Building and testing
# from ZB.MOM.WW.Health/
dotnet build ZB.MOM.WW.Health.slnx
dotnet test ZB.MOM.WW.Health.slnx
All three test assemblies run with dotnet test and require no external dependencies — no database and no
external cluster (the cluster-data tests form a single-node in-process Akka cluster on a loopback port
via Akka.TestKit.Xunit2):
| Assembly | Tests |
|---|---|
ZB.MOM.WW.Health.Tests |
25 |
ZB.MOM.WW.Health.Akka.Tests |
39 |
ZB.MOM.WW.Health.EntityFrameworkCore.Tests |
6 |
| Total | 70 |
Packing
dotnet pack ZB.MOM.WW.Health.slnx -c Release -o ./artifacts
Produces three .nupkg files in artifacts/:
ZB.MOM.WW.Health.0.2.0.nupkg
ZB.MOM.WW.Health.Akka.0.2.0.nupkg
ZB.MOM.WW.Health.EntityFrameworkCore.0.2.0.nupkg
GeneratePackageOnBuild is off — pack explicitly as above.
Status
Built at 0.2.0 and published to the Gitea NuGet feed. Adopted by all four apps (OtOpcUa, MxAccessGateway, ScadaBridge, HistorianGateway). Adoption is tracked in the component backlog:
~/Desktop/scadaproj/components/health/GAPS.md
Design documentation lives alongside that backlog:
~/Desktop/scadaproj/components/health/spec/SPEC.md— normalized three-tier target~/Desktop/scadaproj/components/health/shared-contract/ZB.MOM.WW.Health.md— proposed API~/Desktop/scadaproj/components/health/current-state/— per-project current state (code-verified)