refactor(host)/test: M2.14 review nits — simplify probe cancellation + pre-cancelled-token test (#28)

- 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).
This commit is contained in:
Joseph Doherty
2026-06-16 06:54:28 -04:00
parent 473429a202
commit 6b1cb9e0e6
2 changed files with 35 additions and 3 deletions
@@ -157,11 +157,9 @@ public sealed class RequiredSingletonsHealthCheck : IHealthCheck
// ActorSelection so a missing path resolves an ActorIdentity with a null
// Subject (rather than throwing) within the bounded timeout.
var selection = system.ActorSelection($"/user/{proxyName}");
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(ProbeTimeout);
var identity = await selection
.Ask<ActorIdentity>(new Identify(proxyName), ProbeTimeout, cts.Token)
.Ask<ActorIdentity>(new Identify(proxyName), ProbeTimeout, cancellationToken)
.ConfigureAwait(false);
return (proxyName, identity.Subject is not null);
@@ -27,6 +27,10 @@ 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&lt;Identify&gt;</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
{
}
@@ -106,4 +110,34 @@ public class RequiredSingletonsHealthCheckTests : TestKit
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);
}
}