fix(health): site /health/active uses the shared role-scoped ActiveNodeHealthCheck (ScadaBridge#97)
Replace the private SitePairActiveNodeHealthCheck (503 whenever no ActorSystem, no data)
with the shared ZB.MOM.WW.Health ActiveNodeHealthCheck constructed with the site-{SiteId}
role: Degraded (200) during startup per the Health spec, and it publishes
selfAddress/activeRole/activeNode so the Overview dashboard can name the active site node.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// /health/active check for a SITE pair: Healthy only on the pair's primary — the oldest Up member
|
||||
/// carrying the <c>site-{SiteId}</c> role. Same active/standby semantics central publishes, so a
|
||||
/// consumer reads one contract fleet-wide (200 = Active, 503 = Standby).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately NOT central's unscoped shared <c>ActiveNodeHealthCheck</c>: that one calls
|
||||
/// <c>ClusterActivityEvaluator.SelfIsOldest(cluster)</c> with no role argument, so on a mesh that
|
||||
/// carries more than one site's members it computes "oldest" across the wrong member set and a site
|
||||
/// node could report Active because it happens to be the oldest member overall. Delegating to
|
||||
/// <see cref="IClusterNodeProvider.SelfIsPrimary"/> reuses the role-scoped evaluation the site
|
||||
/// already trusts for singleton placement and event-log purge — one primary definition per site,
|
||||
/// not two that can disagree.
|
||||
/// </remarks>
|
||||
public sealed class SitePairActiveNodeHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IClusterNodeProvider _nodeProvider;
|
||||
|
||||
/// <summary>Initializes a new <see cref="SitePairActiveNodeHealthCheck"/>.</summary>
|
||||
/// <param name="nodeProvider">The role-scoped site cluster node provider.</param>
|
||||
public SitePairActiveNodeHealthCheck(IClusterNodeProvider nodeProvider) =>
|
||||
_nodeProvider = nodeProvider ?? throw new ArgumentNullException(nameof(nodeProvider));
|
||||
|
||||
/// <summary>Reports Healthy on the site pair's primary and Unhealthy on the standby.</summary>
|
||||
/// <param name="context">The health check context (unused; the verdict depends only on cluster membership).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Healthy on the primary; Unhealthy (503 = Standby) otherwise.</returns>
|
||||
public Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(_nodeProvider.SelfIsPrimary
|
||||
? HealthCheckResult.Healthy("site pair primary (oldest Up member of the site role)")
|
||||
: HealthCheckResult.Unhealthy("not the site pair primary (standby)"));
|
||||
}
|
||||
@@ -117,7 +117,7 @@ public static class LoggerConfigurationFactory
|
||||
/// <b>The problem.</b> <c>DefaultHealthCheckService</c> logs one line per check per probe,
|
||||
/// picking the level from the verdict — Debug for Healthy, Warning for Degraded,
|
||||
/// <b>Error for Unhealthy</b>. Both active-node checks
|
||||
/// (<c>SitePairActiveNodeHealthCheck</c> on a site, the shared <c>ActiveNodeHealthCheck</c>
|
||||
/// (the shared <c>ActiveNodeHealthCheck</c>, role-scoped on a site and unscoped
|
||||
/// on central) answer Unhealthy on the standby, because 503 is exactly the contract
|
||||
/// <c>/health/active</c> publishes to Traefik and the family dashboard. So a perfectly
|
||||
/// healthy standby logged ERR on every probe — every 10 s on a site node, every 5 s on a
|
||||
|
||||
@@ -274,12 +274,17 @@ public static class SiteServiceRegistration
|
||||
"localdb",
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Ready })
|
||||
// NOT central's unscoped shared ActiveNodeHealthCheck — that one is role-less; see
|
||||
// SitePairActiveNodeHealthCheck for why a site pair needs the role-scoped answer.
|
||||
.AddTypeActivatedCheck<SitePairActiveNodeHealthCheck>(
|
||||
// The shared ActiveNodeHealthCheck, ROLE-SCOPED to this site's own `site-{SiteId}` role
|
||||
// (ScadaBridge#97). Central registers it unscoped; a site must scope it, because on a
|
||||
// mesh carrying more than one site's members an unscoped "oldest Up member" is computed
|
||||
// over the wrong set. The shared check is startup-safe (Degraded = 200 until the
|
||||
// ActorSystem/cluster is available) and publishes `selfAddress` / `activeRole` /
|
||||
// `activeNode` data, which is what the Overview dashboard reads to place the Active chip.
|
||||
.AddTypeActivatedCheck<ActiveNodeHealthCheck>(
|
||||
"active-node",
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Active });
|
||||
tags: new[] { ZbHealthTags.Active },
|
||||
args: new object[] { $"site-{config["ScadaBridge:Node:SiteId"]}" });
|
||||
|
||||
// Options binding
|
||||
BindSharedOptions(services, config);
|
||||
|
||||
Reference in New Issue
Block a user