5439f14804
Phase 2 Task 0. Binds a validated `MeshTransport` section selecting which transport carries central->node commands, defaulting to Dps so nothing changes until the rig gate passes. The validator exists because every fault it catches otherwise surfaces as an ABSENCE -- a deployment that never arrives, an alarm ack that does nothing -- which has no stack trace and no failing node to point at. Two of the shapes it rejects are ported from the sister project's shipped mistakes: a contact point carrying an actor-path suffix, and (documented in the options XML) a template listing the node's OWN remoting port as a central contact, which is a permanent failure in the initial-contact rotation. Contact points are required only under ClusterClient mode; requiring them under the default would make the section mandatory on admin-only nodes that have no reason to carry them. Sabotage-verified: relaxing the empty-contacts guard and the actor-path-suffix guard turns exactly those two tests red, and nothing else. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
84 lines
4.0 KiB
C#
84 lines
4.0 KiB
C#
using Microsoft.Extensions.Options;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Cluster;
|
|
|
|
/// <summary>
|
|
/// Fails the host at startup on a <see cref="MeshTransportOptions"/> shape that would leave
|
|
/// commands silently undelivered.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Every fault caught here otherwise surfaces as an <i>absence</i> — a deployment that never
|
|
/// arrives, an alarm acknowledgement that does nothing, a node whose ack is dropped without a
|
|
/// trace. An absence has no stack trace and no failing node to point at, which makes it the
|
|
/// most expensive class of misconfiguration to diagnose in the field. Refusing to start is
|
|
/// cheaper for everyone.
|
|
/// </remarks>
|
|
public sealed class MeshTransportOptionsValidator : IValidateOptions<MeshTransportOptions>
|
|
{
|
|
/// <inheritdoc />
|
|
public ValidateOptionsResult Validate(string? name, MeshTransportOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
var errors = new List<string>();
|
|
|
|
var isDps = string.Equals(options.Mode, MeshTransportOptions.ModeDps, StringComparison.OrdinalIgnoreCase);
|
|
var isClusterClient = string.Equals(
|
|
options.Mode, MeshTransportOptions.ModeClusterClient, StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!isDps && !isClusterClient)
|
|
{
|
|
errors.Add(
|
|
$"{MeshTransportOptions.SectionName}:{nameof(MeshTransportOptions.Mode)} is "
|
|
+ $"'{options.Mode}'. Expected '{MeshTransportOptions.ModeDps}' or "
|
|
+ $"'{MeshTransportOptions.ModeClusterClient}'.");
|
|
}
|
|
|
|
if (options.ContactRefreshSeconds <= 0)
|
|
{
|
|
errors.Add(
|
|
$"{MeshTransportOptions.SectionName}:{nameof(MeshTransportOptions.ContactRefreshSeconds)} "
|
|
+ $"must be greater than 0 (was {options.ContactRefreshSeconds}). Central rebuilds its "
|
|
+ "ClusterClient contact points on this interval; a non-positive value would leave the "
|
|
+ "contact set frozen at whatever it held when the node booted.");
|
|
}
|
|
|
|
// Contact points are only load-bearing under ClusterClient mode. Requiring them under the
|
|
// default would make the section mandatory everywhere for a feature that is switched off —
|
|
// including on admin-only nodes, which have no reason to carry them at all.
|
|
if (isClusterClient)
|
|
{
|
|
if (options.CentralContactPoints.Length == 0)
|
|
{
|
|
errors.Add(
|
|
$"{MeshTransportOptions.SectionName}:{nameof(MeshTransportOptions.CentralContactPoints)} "
|
|
+ "is empty. Under ClusterClient mode a driver node cannot reach central without at "
|
|
+ "least one contact point, and its deployment acks would be dropped silently — the "
|
|
+ "deployment would then time out naming a node that is running perfectly.");
|
|
}
|
|
|
|
foreach (var contactPoint in options.CentralContactPoints)
|
|
{
|
|
if (!contactPoint.StartsWith("akka.tcp://", StringComparison.Ordinal))
|
|
{
|
|
errors.Add(
|
|
$"Contact point '{contactPoint}' must start with 'akka.tcp://' — it is an Akka "
|
|
+ "remoting address (akka.tcp://<system>@<host>:<port>), not a bare host:port.");
|
|
}
|
|
else if (contactPoint.Contains("/user/", StringComparison.Ordinal)
|
|
|| contactPoint.Contains("/system/", StringComparison.Ordinal))
|
|
{
|
|
errors.Add(
|
|
$"Contact point '{contactPoint}' carries an actor-path suffix. Configure the "
|
|
+ "node address only — '/system/receptionist' is appended at construction time, "
|
|
+ "and a doubled suffix produces a path that never resolves.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return errors.Count == 0
|
|
? ValidateOptionsResult.Success
|
|
: ValidateOptionsResult.Fail(string.Join(" ", errors));
|
|
}
|
|
}
|