using Microsoft.Extensions.Options; namespace ZB.MOM.WW.OtOpcUa.Cluster; /// /// Fails the host at startup on a shape that would leave /// commands silently undelivered. /// /// /// Every fault caught here otherwise surfaces as an absence — 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. /// public sealed class MeshTransportOptionsValidator : IValidateOptions { /// public ValidateOptionsResult Validate(string? name, MeshTransportOptions options) { ArgumentNullException.ThrowIfNull(options); var errors = new List(); 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://@:), 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)); } }