f5579e6ef2
A failed SDK start (port in use, PKI unwritable, bad OpcUa:* override) was one Error line: the
node stayed /health/ready 200, joined its mesh, could be elected driver Primary and acked deploys
through the Null sink while every client got connection-refused.
- IOpcUaEndpointState / OpcUaEndpointState: the start outcome is a fact the hosted service
maintains (NotStarted / Started / Failed / Stopped + reason).
- OpcUaEndpointHealthCheck ("opcua-endpoint", Ready + Active) is Unhealthy unless Started;
registered by AddOtOpcUaHealth(hasAdmin, hasDriver) on driver-role nodes.
- A driver-only node rethrows the start failure (Program.cs's fatal handler exits non-zero so
the supervisor restarts it); a fused node keeps its AdminUI up but is visibly not ready.
- StopAsync detaches the sinks and then STOPS the SDK (the port closes, clients get
connection-refused rather than a stale, inert endpoint); DisposeAsync is idempotent with it.
- The ServiceLevel calculator / redundancy election live in Cluster + Runtime (not this lane);
the fact is exposed for the Wave 3 follow-up to consume.
Tests: port-in-use ⇒ state Failed, probe Unhealthy, real /health/ready and /health/active 503
(positive control: 200 once Started); driver-only rethrows; StopAsync closes the port and
DisposeAsync twice is a no-op; the probe registration is on both tiers for driver nodes only.
Refs #733
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
88 lines
3.6 KiB
C#
88 lines
3.6 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using Microsoft.Extensions.Options;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.Health;
|
|
using ZB.MOM.WW.Health.Akka;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Health;
|
|
|
|
/// <summary>
|
|
/// Pins OtOpcUa's active-tier wiring. The <i>rule</i> (oldest Up member of a role) now lives in the
|
|
/// shared <c>ClusterActiveNode</c> and is tested there against a real two-node cluster; what stays
|
|
/// OtOpcUa's own decision — and what regressed in <c>lmxopcua#494</c> — is <b>which check is
|
|
/// registered on the active tag, and with which roles</b>.
|
|
/// </summary>
|
|
public sealed class ActiveTierRegistrationTests
|
|
{
|
|
private static HealthCheckRegistration ClusterPrimaryRegistration(bool hasAdmin) =>
|
|
new ServiceCollection()
|
|
.AddOtOpcUaHealth(hasAdmin)
|
|
.BuildServiceProvider()
|
|
.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
|
|
.Value.Registrations
|
|
.Where(r => r.Name == "cluster-primary")
|
|
.ShouldHaveSingleItem();
|
|
|
|
[Fact]
|
|
public void Active_tier_uses_the_shared_ActiveNodeHealthCheck()
|
|
{
|
|
// Not a bespoke copy. Two apps in the family independently hand-rolled this check because the
|
|
// shared one selected by RoleLeader; since Health 0.3.0 the shared one is the correct rule, so
|
|
// a private reimplementation reappearing here would be the regression.
|
|
var registration = ClusterPrimaryRegistration(hasAdmin: true);
|
|
|
|
registration.Factory(new ServiceCollection().BuildServiceProvider())
|
|
.ShouldBeOfType<ActiveNodeHealthCheck>();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Active_tier_check_is_registered_on_every_node_role(bool hasAdmin)
|
|
{
|
|
// Registered on driver-only nodes too, unlike the admin-only configdb probe. A site pair needs
|
|
// an active tier just as much as the central pair does — under the old admin-scoped check all
|
|
// four driver-only site nodes answered 200 and no consumer could find a site's Primary.
|
|
ClusterPrimaryRegistration(hasAdmin).Tags.ShouldContain(ZbHealthTags.Active);
|
|
}
|
|
|
|
/// <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.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Opcua_endpoint_probe_is_registered_on_ready_and_active_for_driver_nodes_only(bool hasDriver)
|
|
{
|
|
var registrations = new ServiceCollection()
|
|
.AddOtOpcUaHealth(hasAdmin: true, hasDriver: hasDriver)
|
|
.BuildServiceProvider()
|
|
.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
|
|
.Value.Registrations
|
|
.Where(r => r.Name == OpcUaEndpointHealthCheck.Name)
|
|
.ToList();
|
|
|
|
if (!hasDriver)
|
|
{
|
|
registrations.ShouldBeEmpty();
|
|
return;
|
|
}
|
|
|
|
var registration = registrations.ShouldHaveSingleItem();
|
|
registration.Tags.ShouldContain(ZbHealthTags.Ready);
|
|
registration.Tags.ShouldContain(ZbHealthTags.Active);
|
|
}
|
|
|
|
[Fact]
|
|
public void Active_tier_check_is_not_on_the_ready_tier()
|
|
{
|
|
// Readiness must not depend on being the active node: a healthy standby is ready to serve, and
|
|
// tagging it ready would take the whole pair out of rotation whenever one node is standby.
|
|
ClusterPrimaryRegistration(hasAdmin: true).Tags.ShouldNotContain(ZbHealthTags.Ready);
|
|
}
|
|
}
|