fix(host): OPC UA endpoint probe is Ready-only on a fused admin+driver node
Coordinator ruling (I6): until ServiceLevel consumes IOpcUaEndpointState (F-C1), the probe must not sit on the Active tier of a fused node, because Traefik routes the AdminUI on /health/active. Driver-only nodes keep it on Ready + Active; admin-only nodes do not register it. Pinned per role. Refs #733 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -31,7 +31,8 @@ public static class HealthEndpoints
|
||||
/// </param>
|
||||
/// <param name="hasDriver">
|
||||
/// Whether this node carries the <c>driver</c> role and therefore serves an OPC UA endpoint. When
|
||||
/// <c>true</c> the <see cref="OpcUaEndpointHealthCheck"/> joins the ready + active tiers (R03:2.1), reading
|
||||
/// <c>true</c> the <see cref="OpcUaEndpointHealthCheck"/> joins the ready tier, and the active tier on a
|
||||
/// driver-ONLY node (R03:2.1; on a fused node the active tier routes the AdminUI — see the body), reading
|
||||
/// the <see cref="IOpcUaEndpointState"/> the host registers alongside <c>OtOpcUaServerHostedService</c>.
|
||||
/// Defaults to <c>false</c> so admin-only callers and the existing harnesses register nothing new.
|
||||
/// </param>
|
||||
@@ -40,16 +41,26 @@ public static class HealthEndpoints
|
||||
{
|
||||
var checks = services.AddHealthChecks();
|
||||
|
||||
// A driver node's whole purpose is its OPC UA endpoint, so a failed SDK start must take it out of
|
||||
// the ready AND active tiers: readiness because it cannot serve, active because a load balancer or
|
||||
// the fleet must not route to it. Factory-registered so the state singleton is resolved per probe.
|
||||
// A failed SDK start takes the node out of the READY tier on every driver node: it cannot serve.
|
||||
// The ACTIVE tier depends on the roles (coordinator ruling, fix round 1 / I6):
|
||||
// - driver-only: Active too — a site node's whole purpose is its OPC UA endpoint, so nothing may
|
||||
// route to it;
|
||||
// - fused admin+driver: NOT Active, until the ServiceLevel calculator consumes IOpcUaEndpointState
|
||||
// (follow-up F-C1). Traefik routes the AdminUI on /health/active, so putting the probe there would
|
||||
// take the operator's only console out of rotation precisely when they need it to diagnose the
|
||||
// failed endpoint, while the OPC UA failover (ServiceLevel) still would not move. Once F-C1 lands
|
||||
// the peer takes Primary and the active tier follows it on its own.
|
||||
// Factory-registered so the state singleton is resolved per probe.
|
||||
if (hasDriver)
|
||||
{
|
||||
var tags = hasAdmin
|
||||
? new[] { ZbHealthTags.Ready }
|
||||
: new[] { ZbHealthTags.Ready, ZbHealthTags.Active };
|
||||
checks.Add(new HealthCheckRegistration(
|
||||
OpcUaEndpointHealthCheck.Name,
|
||||
sp => new OpcUaEndpointHealthCheck(sp.GetRequiredService<IOpcUaEndpointState>()),
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active }));
|
||||
tags: tags));
|
||||
}
|
||||
|
||||
// configdb probe is admin-only (per-cluster mesh Phase 4). A driver-only node gets its config
|
||||
|
||||
@@ -4,14 +4,16 @@ using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// Ready + Active tier probe for the node's OPC UA endpoint (R03:2.1): Healthy only while the SDK is
|
||||
/// Ready-tier probe (plus Active on a driver-only node) for the node's OPC UA endpoint (R03:2.1): Healthy only while the SDK is
|
||||
/// listening. A driver node whose SDK failed to start is otherwise indistinguishable from a healthy
|
||||
/// one over HTTP — the configdb and akka probes are both green — while every OPC UA client gets
|
||||
/// connection-refused.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Registered on driver-role nodes only (<see cref="HealthEndpoints.AddOtOpcUaHealth"/>); an
|
||||
/// admin-only node serves no OPC UA endpoint and must not be degraded by one it never starts.
|
||||
/// admin-only node serves no OPC UA endpoint and must not be degraded by one it never starts. On a fused
|
||||
/// admin+driver node it is Ready-only until ServiceLevel consumes the fact (F-C1), because the Active tier
|
||||
/// is what Traefik routes the AdminUI on.
|
||||
/// </remarks>
|
||||
public sealed class OpcUaEndpointHealthCheck : IHealthCheck
|
||||
{
|
||||
|
||||
@@ -50,31 +50,38 @@ public sealed class ActiveTierRegistrationTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R03:2.1 — the OPC UA endpoint probe is on BOTH tiers of a driver node (it cannot serve, and must not
|
||||
/// be routed to) and absent from an admin-only node, which starts no endpoint to be degraded by.
|
||||
/// R03:2.1 + fix round 1 / I6 (coordinator ruling) — the OPC UA endpoint probe's tiers by role:
|
||||
/// <list type="bullet">
|
||||
/// <item>driver-only: Ready + Active (it cannot serve and must not be routed to);</item>
|
||||
/// <item>fused admin+driver: Ready ONLY, until the ServiceLevel calculator consumes the fact
|
||||
/// (F-C1) — Traefik routes the AdminUI on <c>/health/active</c>, so a failed SDK on the active
|
||||
/// central node must not take the AdminUI out of rotation;</item>
|
||||
/// <item>admin-only: absent (it starts no endpoint to be degraded by).</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Opcua_endpoint_probe_is_registered_on_ready_and_active_for_driver_nodes_only(bool hasDriver)
|
||||
[InlineData(false, true, true, true)]
|
||||
[InlineData(true, true, true, false)]
|
||||
[InlineData(true, false, false, false)]
|
||||
public void Opcua_endpoint_probe_tiers_follow_the_node_roles(bool hasAdmin, bool hasDriver, bool ready, bool active)
|
||||
{
|
||||
var registrations = new ServiceCollection()
|
||||
.AddOtOpcUaHealth(hasAdmin: true, hasDriver: hasDriver)
|
||||
.AddOtOpcUaHealth(hasAdmin: hasAdmin, hasDriver: hasDriver)
|
||||
.BuildServiceProvider()
|
||||
.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
|
||||
.Value.Registrations
|
||||
.Where(r => r.Name == OpcUaEndpointHealthCheck.Name)
|
||||
.ToList();
|
||||
|
||||
if (!hasDriver)
|
||||
if (!ready && !active)
|
||||
{
|
||||
registrations.ShouldBeEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
var registration = registrations.ShouldHaveSingleItem();
|
||||
registration.Tags.ShouldContain(ZbHealthTags.Ready);
|
||||
registration.Tags.ShouldContain(ZbHealthTags.Active);
|
||||
registration.Tags.Contains(ZbHealthTags.Ready).ShouldBe(ready);
|
||||
registration.Tags.Contains(ZbHealthTags.Active).ShouldBe(active);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user