using System.Net.Sockets; using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Akka.Event; using ZB.MOM.WW.OtOpcUa.Commons.Types; namespace ZB.MOM.WW.OtOpcUa.Runtime.Health; /// /// Periodically pings a peer node's OPC UA endpoint (opc.tcp://peer:4840) and publishes /// the result on the cluster's redundancy-state input topic so the admin RedundancyStateActor /// can react. /// /// The probe is a plain TCP connect to the OPC UA port with a short timeout — enough to detect /// "is the OPC UA server process up and accepting connections?" A full secure-channel handshake /// (Hello / Acknowledge) needs the OPC UA Client SDK and a session/PKI setup, which is more than /// what the redundancy calculator needs. Upgrade to a real Hello probe if a deeper liveness signal /// is ever required. /// public sealed class PeerOpcUaProbeActor : ReceiveActor, IWithTimers { // Owned by ControlPlane.Redundancy.RedundancyStateActor; duplicated here to avoid a // Runtime → ControlPlane project reference. Keep both literals in lock-step. public const string RedundancyStateTopic = "redundancy-state"; public static readonly TimeSpan DefaultProbeInterval = TimeSpan.FromSeconds(10); public static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(2); public const int DefaultOpcUaPort = 4840; public sealed record OpcUaProbeResult(NodeId NodeId, bool Ok); public sealed class Tick { public static readonly Tick Instance = new(); private Tick() { } } private readonly NodeId _peer; private readonly TimeSpan _interval; private readonly TimeSpan _connectTimeout; private readonly int _opcUaPort; private readonly Action? _broadcastOverride; private readonly ILoggingAdapter _log = Context.GetLogger(); /// Gets or sets the timer scheduler for this actor. public ITimerScheduler Timers { get; set; } = null!; /// Creates actor props for the peer OPC UA probe. /// The node identifier of the peer to probe. /// Optional probe interval; defaults to DefaultProbeInterval. /// Optional connection timeout; defaults to DefaultConnectTimeout. /// The OPC UA port to connect to; defaults to DefaultOpcUaPort. /// Optional custom broadcast delegate for probe results. /// Props configured to create a PeerOpcUaProbeActor. public static Props Props( NodeId peer, TimeSpan? interval = null, TimeSpan? connectTimeout = null, int opcUaPort = DefaultOpcUaPort, Action? broadcast = null) => Akka.Actor.Props.Create(() => new PeerOpcUaProbeActor( peer, interval ?? DefaultProbeInterval, connectTimeout ?? DefaultConnectTimeout, opcUaPort, broadcast)); /// Initializes a new instance of the PeerOpcUaProbeActor. /// The node identifier of the peer to probe. /// The probe interval. /// The connection timeout. /// The OPC UA port to connect to. /// Optional custom broadcast delegate for probe results. public PeerOpcUaProbeActor( NodeId peer, TimeSpan interval, TimeSpan connectTimeout, int opcUaPort, Action? broadcastOverride) { _peer = peer; _interval = interval; _connectTimeout = connectTimeout; _opcUaPort = opcUaPort; _broadcastOverride = broadcastOverride; ReceiveAsync(_ => RunProbeAsync()); } /// protected override void PreStart() => Timers.StartPeriodicTimer("probe", Tick.Instance, _interval); private async Task RunProbeAsync() { var host = ExtractHost(_peer); var ok = await TryTcpConnectAsync(host, _opcUaPort, _connectTimeout); var msg = new OpcUaProbeResult(_peer, ok); if (_broadcastOverride is not null) _broadcastOverride(msg); else DistributedPubSub.Get(Context.System).Mediator.Tell(new Publish(RedundancyStateTopic, msg)); _log.Debug("PeerOpcUaProbe: pinged {Peer} ({Host}:{Port}) → ok={Ok}", _peer, host, _opcUaPort, ok); } /// /// values are derived as host:port (see ClusterRoleInfo). /// Strip the port suffix to get the hostname for the TCP connect. /// private static string ExtractHost(NodeId nodeId) { var s = nodeId.Value; var colon = s.LastIndexOf(':'); return colon > 0 ? s[..colon] : s; } private static async Task TryTcpConnectAsync(string host, int port, TimeSpan timeout) { try { using var client = new TcpClient(); using var cts = new CancellationTokenSource(timeout); await client.ConnectAsync(host, port, cts.Token); return client.Connected; } catch (Exception) { return false; } } }