6b1cb9e0e6
- Remove redundant linked CancellationTokenSource in ProbeAsync; pass the framework cancellationToken and ProbeTimeout directly to Ask (the two-CTS pattern was redundant — Ask already honours both the timeout and the token). - Add EchoActor XML <remarks> explaining why no Receive<Identify> handler is needed (ActorBase answers Identify automatically). - Add PreCancelledToken_ReportsUnhealthy_DoesNotThrow test: verifies the never-throws guarantee on the shutdown-race path (token already cancelled before CheckHealthAsync is invoked).
144 lines
5.7 KiB
C#
144 lines
5.7 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.ScadaBridge.Host.Health;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// M2.14 (#28): unit tests for <see cref="RequiredSingletonsHealthCheck"/>.
|
|
///
|
|
/// The check probes each required central singleton through its local
|
|
/// <c>ClusterSingletonProxy</c> by Asking an <see cref="Identify"/> with a short
|
|
/// bounded timeout and treating a non-null <see cref="ActorIdentity.Subject"/> as
|
|
/// "reachable". These tests exercise that probe logic directly against a TestKit
|
|
/// <see cref="ActorSystem"/>:
|
|
/// <list type="bullet">
|
|
/// <item>present + reachable proxy paths (live echo actors) → Healthy;</item>
|
|
/// <item>a missing proxy path (ActorSelection resolves a null Subject) → Unhealthy
|
|
/// naming the unreachable singleton.</item>
|
|
/// </list>
|
|
/// No WebApplicationFactory / DB / formed cluster is needed — the probe is just an
|
|
/// in-process Identify round-trip, so the tests are deterministic and fast.
|
|
/// </summary>
|
|
public class RequiredSingletonsHealthCheckTests : TestKit
|
|
{
|
|
/// <summary>A minimal live actor that does nothing — its mere existence makes
|
|
/// an <see cref="Identify"/> resolve a non-null Subject (i.e. "reachable").</summary>
|
|
/// <remarks>No <c>Receive<Identify></c> handler is needed: Akka's
|
|
/// <see cref="ActorBase"/> answers every <see cref="Identify"/> message with
|
|
/// an <see cref="ActorIdentity"/> automatically, so an empty actor at the proxy
|
|
/// path is sufficient to simulate a reachable singleton.</remarks>
|
|
private sealed class EchoActor : ReceiveActor
|
|
{
|
|
}
|
|
|
|
private IServiceProvider ProviderReturning(ActorSystem system)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(system);
|
|
return services.BuildServiceProvider();
|
|
}
|
|
|
|
private static async Task<HealthCheckResult> RunAsync(RequiredSingletonsHealthCheck check)
|
|
{
|
|
var context = new HealthCheckContext
|
|
{
|
|
Registration = new HealthCheckRegistration(
|
|
"required-singletons", check, failureStatus: null, tags: null),
|
|
};
|
|
return await check.CheckHealthAsync(context, CancellationToken.None);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllRequiredSingletonProxiesReachable_ReportsHealthy()
|
|
{
|
|
// Create a live actor at every required proxy path so each Identify resolves
|
|
// a non-null Subject.
|
|
foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
|
|
{
|
|
Sys.ActorOf(Props.Create(() => new EchoActor()), name);
|
|
}
|
|
|
|
var check = new RequiredSingletonsHealthCheck(
|
|
ProviderReturning(Sys),
|
|
NullLogger<RequiredSingletonsHealthCheck>.Instance);
|
|
|
|
var result = await RunAsync(check);
|
|
|
|
Assert.Equal(HealthStatus.Healthy, result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OneRequiredSingletonUnreachable_ReportsUnhealthyNamingIt()
|
|
{
|
|
// Create all but one proxy. The missing one's ActorSelection resolves an
|
|
// ActorIdentity with a null Subject within the bounded timeout → unreachable.
|
|
var missing = RequiredSingletonsHealthCheck.RequiredSingletonProxyNames[0];
|
|
foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
|
|
{
|
|
if (name == missing)
|
|
continue;
|
|
Sys.ActorOf(Props.Create(() => new EchoActor()), name);
|
|
}
|
|
|
|
var check = new RequiredSingletonsHealthCheck(
|
|
ProviderReturning(Sys),
|
|
NullLogger<RequiredSingletonsHealthCheck>.Instance);
|
|
|
|
var result = await RunAsync(check);
|
|
|
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
|
Assert.NotNull(result.Description);
|
|
Assert.Contains(missing, result.Description!);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ActorSystemNotYetAvailable_ReportsUnhealthy_DoesNotThrow()
|
|
{
|
|
// Startup race: ActorSystem not yet bridged into DI. The check must map this
|
|
// to Unhealthy (the node is not ready to serve) rather than throwing.
|
|
var emptyProvider = new ServiceCollection().BuildServiceProvider();
|
|
|
|
var check = new RequiredSingletonsHealthCheck(
|
|
emptyProvider,
|
|
NullLogger<RequiredSingletonsHealthCheck>.Instance);
|
|
|
|
var result = await RunAsync(check);
|
|
|
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreCancelledToken_ReportsUnhealthy_DoesNotThrow()
|
|
{
|
|
// Shutdown-race path: CheckHealthAsync is called with an already-cancelled
|
|
// token (e.g. host is tearing down). The check must never throw — any
|
|
// OperationCanceledException from Ask must be caught and mapped to Unhealthy.
|
|
foreach (var name in RequiredSingletonsHealthCheck.RequiredSingletonProxyNames)
|
|
{
|
|
Sys.ActorOf(Props.Create(() => new EchoActor()), name);
|
|
}
|
|
|
|
var check = new RequiredSingletonsHealthCheck(
|
|
ProviderReturning(Sys),
|
|
NullLogger<RequiredSingletonsHealthCheck>.Instance);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
cts.Cancel(); // already cancelled before the check runs
|
|
|
|
var context = new HealthCheckContext
|
|
{
|
|
Registration = new HealthCheckRegistration(
|
|
"required-singletons", check, failureStatus: null, tags: null),
|
|
};
|
|
|
|
// Must not throw; an already-cancelled token → all probes fail → Unhealthy.
|
|
var result = await check.CheckHealthAsync(context, cts.Token);
|
|
|
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
|
}
|
|
}
|