feat(runtime): PeerOpcUaProbeActor real TCP-connect probe (F12)

Replaces the Ok=true stub with a TCP connect to the peer's OPC UA port (4840
default) with a 2s timeout. A successful connect indicates the OPC UA server
process is up + accepting connections — enough for the redundancy calculator
to treat the peer as live. A full secure-channel Hello/Acknowledge handshake
is overkill for what the redundancy calc consumes and would pull in the OPC
UA Client SDK + a PKI setup. Upgrade later if a deeper liveness signal is ever
required.

Probe extracts the host from NodeId by stripping the :port suffix (commit
5cfbe8b encoded host:port into NodeId for cluster-member identity).

Tests: 2 new tests — Ok=true against a live TcpListener on a chosen port,
Ok=false against an unreachable endpoint. All 17 Runtime tests pass (was 16
covering only the message-contract surface).
This commit is contained in:
Joseph Doherty
2026-05-26 06:54:51 -04:00
parent f57f61deac
commit b06e3ae740
3 changed files with 96 additions and 16 deletions

View File

@@ -1,3 +1,5 @@
using System.Net;
using System.Net.Sockets;
using Akka.Actor;
using Shouldly;
using Xunit;
@@ -24,16 +26,38 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
}
[Fact]
public void PeerOpcUaProbeActor_publishes_probe_result_at_each_tick()
public void PeerOpcUaProbeActor_reports_Ok_true_against_a_live_listener()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
var received = new System.Collections.Generic.List<object>();
var actor = Sys.ActorOf(PeerOpcUaProbeActor.Props(
NodeId.Parse("peer-1"),
Sys.ActorOf(PeerOpcUaProbeActor.Props(
NodeId.Parse($"127.0.0.1:{port}"),
interval: TimeSpan.FromMilliseconds(50),
connectTimeout: TimeSpan.FromMilliseconds(500),
opcUaPort: port,
broadcast: msg => received.Add(msg)));
AwaitCondition(() => received.Count >= 2, TimeSpan.FromSeconds(2));
received.OfType<PeerOpcUaProbeActor.OpcUaProbeResult>().ShouldNotBeEmpty();
AwaitCondition(() => received.OfType<PeerOpcUaProbeActor.OpcUaProbeResult>().Any(r => r.Ok),
TimeSpan.FromSeconds(3));
}
[Fact]
public void PeerOpcUaProbeActor_reports_Ok_false_against_an_unreachable_endpoint()
{
// Port 1 is reserved (tcpmux) and almost never bound on dev machines, so the connect fails fast.
var received = new System.Collections.Generic.List<object>();
Sys.ActorOf(PeerOpcUaProbeActor.Props(
NodeId.Parse("127.0.0.1:1"),
interval: TimeSpan.FromMilliseconds(50),
connectTimeout: TimeSpan.FromMilliseconds(300),
opcUaPort: 1,
broadcast: msg => received.Add(msg)));
AwaitCondition(() => received.OfType<PeerOpcUaProbeActor.OpcUaProbeResult>().Any(r => !r.Ok),
TimeSpan.FromSeconds(3));
}
[Fact]