fix(health): akka check uses the shared SPEC Default policy, not Degraded-only OtOpcUaCompat (lmxopcua#771)

OtOpcUaCompat was a migration shim (1d729fb0) preserving the pre-adoption
two-way mapping; nothing depends on it. A node that is not a live cluster
member now fails /health/ready.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:13:16 -05:00
parent 90db63cfaf
commit 3d26907b1a
2 changed files with 36 additions and 1 deletions
@@ -84,7 +84,11 @@ public static class HealthEndpoints
"akka",
failureStatus: null,
tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active },
args: AkkaClusterStatusPolicy.OtOpcUaCompat)
// Shared SPEC Default policy (lmxopcua#771). OtOpcUaCompat was a migration shim that kept the
// pre-adoption two-way behaviour (anything not Up = Degraded), which can never fail /health/ready
// — Degraded passes. Nothing relies on that: Down/Removed/Joining-failed now answer 503 on the
// ready tier, Leaving/Exiting stay Degraded (a draining node keeps serving).
args: AkkaClusterStatusPolicy.Default)
// "Is this node in charge of its mesh?" — Healthy on the one node that owns the active
// work for its Cluster, Unhealthy (503) on its partner, so Traefik pins the AdminUI to the
// node actually hosting the cluster singletons.
@@ -1,3 +1,6 @@
using Akka.Actor;
using Akka.Configuration;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
@@ -92,3 +95,31 @@ public sealed class ActiveTierRegistrationTests
ClusterPrimaryRegistration(hasAdmin: true).Tags.ShouldNotContain(ZbHealthTags.Ready);
}
}
/// <summary>lmxopcua#771 — the akka check must use the shared SPEC Default policy so a node that is not a live
/// cluster member makes <c>/health/ready</c> fail (the retired OtOpcUaCompat policy could only say Degraded).</summary>
public sealed class AkkaReadyPolicyTests : TestKit
{
public AkkaReadyPolicyTests()
: base(ConfigurationFactory.ParseString(
"akka.actor.provider = cluster\nakka.remote.dot-netty.tcp.port = 0\nakka.remote.dot-netty.tcp.hostname = 127.0.0.1"))
{
}
[Fact]
public async Task Akka_check_is_Unhealthy_when_self_has_not_joined_a_cluster()
{
var services = new ServiceCollection();
services.AddSingleton<ActorSystem>(Sys);
services.AddOtOpcUaHealth(hasAdmin: true);
var sp = services.BuildServiceProvider();
var registration = sp.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
.Value.Registrations.Single(r => r.Name == "akka");
registration.Tags.ShouldContain(ZbHealthTags.Ready);
var result = await registration.Factory(sp).CheckHealthAsync(
new HealthCheckContext { Registration = registration }, CancellationToken.None);
result.Status.ShouldBe(HealthStatus.Unhealthy);
}
}