c69a1c441b
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.
139 lines
5.4 KiB
C#
139 lines
5.4 KiB
C#
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
|
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
|
|
|
public sealed class SessionHealthCheckTests
|
|
{
|
|
/// <summary>
|
|
/// An idle gateway is healthy. This is the load-bearing case: a gateway holding no sessions is
|
|
/// the normal steady state on a host nothing dials yet, and a probe that reports red there is
|
|
/// one operators learn to ignore.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Healthy_WhenNoSessionsAreOpen()
|
|
{
|
|
var check = new SessionHealthCheck(new SessionRegistry());
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Healthy, result.Status);
|
|
Assert.Equal(0, result.Data["total"]);
|
|
Assert.Equal("No MXAccess sessions are open.", result.Description);
|
|
}
|
|
|
|
/// <summary>Every session ready reports healthy, with the counts carried as entry data.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Healthy_WhenAllSessionsReady()
|
|
{
|
|
var check = new SessionHealthCheck(RegistryWith(SessionState.Ready, SessionState.Ready));
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Healthy, result.Status);
|
|
Assert.Equal(2, result.Data["total"]);
|
|
Assert.Equal(2, result.Data["ready"]);
|
|
Assert.Equal(0, result.Data["faulted"]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A faulted session alongside a usable one is degraded, not unhealthy — the gateway is still
|
|
/// serving the sessions that work.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Degraded_WhenSomeFaultedAndSomeReady()
|
|
{
|
|
var check = new SessionHealthCheck(RegistryWith(SessionState.Ready, SessionState.Faulted));
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Degraded, result.Status);
|
|
Assert.Equal(1, result.Data["ready"]);
|
|
Assert.Equal(1, result.Data["faulted"]);
|
|
Assert.Contains("1 faulted", result.Description, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A session still starting counts as usable for grading, so a fault beside it is degraded
|
|
/// rather than unhealthy — the startup has not failed yet.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Degraded_WhenFaultedBesideAStartingSession()
|
|
{
|
|
var check = new SessionHealthCheck(
|
|
RegistryWith(SessionState.Faulted, SessionState.StartingWorker));
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Degraded, result.Status);
|
|
Assert.Equal(1, result.Data["starting"]);
|
|
}
|
|
|
|
/// <summary>Every session faulted is the genuinely bad condition, and the only unhealthy one.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Unhealthy_WhenEverySessionIsFaulted()
|
|
{
|
|
var check = new SessionHealthCheck(RegistryWith(SessionState.Faulted, SessionState.Faulted));
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
|
Assert.Equal(2, result.Data["faulted"]);
|
|
Assert.Equal(0, result.Data["ready"]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closed sessions linger in the registry until they are removed. They are counted separately
|
|
/// and excluded from the verdict, so a gateway whose sessions all closed cleanly is healthy —
|
|
/// not unhealthy for having zero ready ones.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Healthy_WhenOnlyClosedSessionsRemain()
|
|
{
|
|
var check = new SessionHealthCheck(RegistryWith(SessionState.Closed, SessionState.Closed));
|
|
|
|
HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
Assert.Equal(HealthStatus.Healthy, result.Status);
|
|
Assert.Equal(2, result.Data["closing"]);
|
|
Assert.Equal(0, result.Data["ready"]);
|
|
}
|
|
|
|
private static SessionRegistry RegistryWith(params SessionState[] states)
|
|
{
|
|
var registry = new SessionRegistry();
|
|
for (int i = 0; i < states.Length; i++)
|
|
{
|
|
GatewaySession session = CreateSession($"session-{i}");
|
|
session.TransitionTo(states[i]);
|
|
Assert.True(registry.TryAdd(session));
|
|
}
|
|
|
|
return registry;
|
|
}
|
|
|
|
private static GatewaySession CreateSession(string sessionId)
|
|
{
|
|
return new GatewaySession(
|
|
sessionId,
|
|
"mxaccess",
|
|
$"mxaccess-gateway-1-{sessionId}",
|
|
"nonce",
|
|
clientIdentity: null,
|
|
clientSessionName: "test-session",
|
|
clientCorrelationId: "client-correlation",
|
|
TimeSpan.FromSeconds(30),
|
|
TimeSpan.FromSeconds(5),
|
|
TimeSpan.FromSeconds(5),
|
|
DateTimeOffset.UnixEpoch);
|
|
}
|
|
}
|