Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/SessionHealthCheck.cs
T
Joseph Doherty c69a1c441b feat(diagnostics): report MXAccess session health on the active probe
Adds a `mxaccess-sessions` health check reporting 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 answers "how many Galaxy connections
are healthy" in the vocabulary the code actually uses.

Zero sessions is Healthy, deliberately, and the rest of the design follows from
that. The gateway opens a session when a client asks and holds none otherwise,
so an idle gateway is working normally. A count threshold ("unhealthy below N")
would sit red forever on a host nothing dials yet, and a permanently red probe
is one operators stop reading — which leaves them worse off than no probe. The
check therefore grades on whether the sessions that exist are usable: nothing
faulted is Healthy, some faulted beside a ready or starting one is Degraded, and
every session faulted is Unhealthy. Counts ride along as entry data for the
family Overview dashboard.

Tagged `active` rather than `ready` for the same reason. Readiness decides
whether the process should be sent traffic, and a gateway with no sessions is
ready to serve — unlike the auth store, which every call depends on. Failing
readiness here would pull a working gateway out of rotation over a condition its
own clients create.

Reads ISessionRegistry, which already exposes Snapshot(); ISessionManager stays
the command surface and grows no enumerator.
2026-08-11 08:41:54 -04:00

115 lines
4.9 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// <b>Zero sessions is healthy, deliberately.</b> 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.
/// </para>
/// <para>
/// This is tagged <c>active</c> rather than <c>ready</c> 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
/// <see cref="AuthStoreHealthCheck"/>). Failing readiness on session state would take a working
/// gateway out of rotation for a condition its own clients cause.
/// </para>
/// </remarks>
public sealed class SessionHealthCheck : IHealthCheck
{
private readonly ISessionRegistry _sessionRegistry;
/// <summary>Initializes a new instance of the <see cref="SessionHealthCheck"/> class.</summary>
/// <param name="sessionRegistry">Registry holding the live sessions.</param>
public SessionHealthCheck(ISessionRegistry sessionRegistry) =>
_sessionRegistry = sessionRegistry ?? throw new ArgumentNullException(nameof(sessionRegistry));
/// <summary>Buckets the live sessions by state and grades the result.</summary>
/// <param name="context">The health check context.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>
/// 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.
/// </returns>
public Task<HealthCheckResult> 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<string, object> 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.";
}
}