using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Sessions; namespace ZB.MOM.WW.MxGateway.Server.Diagnostics; /// /// Reports how many MXAccess sessions are healthy. Each session is one worker process holding one /// MXAccess COM instance — a live connection into a Galaxy — so this is the "how many Galaxy /// connections are healthy" probe, expressed in the vocabulary the code actually uses. /// /// /// /// Zero sessions is healthy, deliberately. The gateway is a server: it opens a session when /// a client asks and holds none otherwise, so idle-with-no-clients is the normal steady state, not /// a fault. A count-based rule ("unhealthy below N") would sit red forever on a host nothing dials /// yet, and a probe that is permanently red is one people learn to ignore — which costs more than /// having no probe. The status here is therefore false only when a session exists and its worker /// has actually failed. /// /// /// This is tagged active rather than ready for the same reason. Readiness gates /// whether the process should receive traffic, and a gateway with no sessions is legitimately ready /// to serve — unlike the auth store, which every call depends on (see /// ). Failing readiness on session state would take a working /// gateway out of rotation for a condition its own clients cause. /// /// public sealed class SessionHealthCheck : IHealthCheck { private readonly ISessionRegistry _sessionRegistry; /// Initializes a new instance of the class. /// Registry holding the live sessions. public SessionHealthCheck(ISessionRegistry sessionRegistry) => _sessionRegistry = sessionRegistry ?? throw new ArgumentNullException(nameof(sessionRegistry)); /// Buckets the live sessions by state and grades the result. /// The health check context. /// Token to cancel the asynchronous operation. /// /// Healthy when nothing is faulted (including when no sessions are open), Degraded when some /// sessions are faulted but others are still usable, and Unhealthy when every session is /// faulted. /// public Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { int ready = 0; int faulted = 0; int starting = 0; int closing = 0; foreach (GatewaySession session in _sessionRegistry.Snapshot()) { switch (session.State) { case SessionState.Ready: ready++; break; case SessionState.Faulted: faulted++; break; case SessionState.Closing: case SessionState.Closed: // Counted but excluded from the verdict: a session on its way out is an // expected lifecycle stage, not a failure, and Snapshot() still returns // Closed sessions until they are removed from the registry. closing++; break; default: // Creating / StartingWorker / WaitingForPipe / Handshaking / // InitializingWorker — mid-startup, not yet usable but not wrong. // Unspecified lands here too; it is the proto zero value and should not occur. starting++; break; } } int total = ready + faulted + starting + closing; int usable = ready + starting; Dictionary data = new(StringComparer.Ordinal) { ["total"] = total, ["ready"] = ready, ["faulted"] = faulted, ["starting"] = starting, ["closing"] = closing, }; HealthCheckResult result = (faulted, usable) switch { (0, _) => HealthCheckResult.Healthy(Describe(total, ready, faulted), data), (_, 0) => HealthCheckResult.Unhealthy(Describe(total, ready, faulted), data: data), _ => HealthCheckResult.Degraded(Describe(total, ready, faulted), data: data), }; return Task.FromResult(result); } private static string Describe(int total, int ready, int faulted) { if (total == 0) { return "No MXAccess sessions are open."; } return faulted == 0 ? $"{ready} of {total} MXAccess sessions ready." : $"{ready} of {total} MXAccess sessions ready, {faulted} faulted."; } }