From 3d26907b1acd3dd5459c52e159e513cd8ede441e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 9 Nov 2026 14:13:16 -0500 Subject: [PATCH] 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 --- .../Health/HealthEndpoints.cs | 6 +++- .../Health/ActiveTierRegistrationTests.cs | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs index 0f69625b..c3a48688 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs @@ -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. diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs index 45ff262b..bca0ea88 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs @@ -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); } } + +/// lmxopcua#771 — the akka check must use the shared SPEC Default policy so a node that is not a live +/// cluster member makes /health/ready fail (the retired OtOpcUaCompat policy could only say Degraded). +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(Sys); + services.AddOtOpcUaHealth(hasAdmin: true); + var sp = services.BuildServiceProvider(); + var registration = sp.GetRequiredService>() + .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); + } +}