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.";
}
}