From 3d0f4dc168396c48374e98019b8debd4acf49d76 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 26 May 2026 04:31:03 -0400 Subject: [PATCH] feat(cluster): embed Akka HOCON config matching ScadaLink tuning --- .../ZB.MOM.WW.OtOpcUa.Cluster/HoconLoader.cs | 15 ++++ .../Resources/akka.conf | 73 +++++++++++++++++++ .../ZB.MOM.WW.OtOpcUa.Cluster.csproj | 4 + 3 files changed, 92 insertions(+) create mode 100644 src/Core/ZB.MOM.WW.OtOpcUa.Cluster/HoconLoader.cs create mode 100644 src/Core/ZB.MOM.WW.OtOpcUa.Cluster/Resources/akka.conf diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/HoconLoader.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/HoconLoader.cs new file mode 100644 index 0000000..23f1b00 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/HoconLoader.cs @@ -0,0 +1,15 @@ +namespace ZB.MOM.WW.OtOpcUa.Cluster; + +public static class HoconLoader +{ + private const string ResourceName = "ZB.MOM.WW.OtOpcUa.Cluster.Resources.akka.conf"; + + public static string LoadBaseConfig() + { + using var stream = typeof(HoconLoader).Assembly.GetManifestResourceStream(ResourceName) + ?? throw new InvalidOperationException( + $"Embedded resource '{ResourceName}' not found. Verify EmbeddedResource glob in csproj."); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/Resources/akka.conf b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/Resources/akka.conf new file mode 100644 index 0000000..46b9711 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/Resources/akka.conf @@ -0,0 +1,73 @@ +# Base Akka.NET cluster configuration for OtOpcUa fused-host nodes. +# +# Roles, seed nodes, public hostname/port, and the actor system name are overlaid +# at runtime by AkkaHostedService — see ZB.MOM.WW.OtOpcUa.Cluster/AkkaHostedService.cs. +# Everything else here is the cluster-wide tuning that should match across nodes. +# +# Tuning sourced from ScadaLink (ScadaLink.Host/Actors/AkkaHostedService.BuildHocon); +# any divergence must be deliberate and recorded in docs/v2/Architecture.md. + +akka { + extensions = [ + "Akka.Cluster.Tools.PublishSubscribe.DistributedPubSubExtensionProvider, Akka.Cluster.Tools" + ] + + actor { + provider = cluster + } + + remote { + dot-netty.tcp { + hostname = "0.0.0.0" + port = 4053 + } + transport-failure-detector { + heartbeat-interval = 2s + acceptable-heartbeat-pause = 10s + } + } + + cluster { + seed-nodes = [] + roles = [] + min-nr-of-members = 1 + + split-brain-resolver { + active-strategy = "keep-oldest" + stable-after = 15s + keep-oldest { + down-if-alone = on + } + } + + failure-detector { + heartbeat-interval = 2s + threshold = 10.0 + acceptable-heartbeat-pause = 10s + } + + down-removal-margin = 15s + run-coordinated-shutdown-when-down = on + + singleton { + singleton-name = "singleton" + } + singleton-proxy { + singleton-identification-interval = 1s + } + } + + coordinated-shutdown { + run-by-clr-shutdown-hook = on + default-phase-timeout = 30s + } +} + +# Pinned dispatcher used by OpcUaPublishActor (Task 44) so the OPC UA SDK sees +# only one thread per actor instance — its session/subscription locks expect +# strict single-threaded access. +opcua-synchronized-dispatcher { + type = "PinnedDispatcher" + executor = "thread-pool-executor" + throughput = 1 +} diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ZB.MOM.WW.OtOpcUa.Cluster.csproj b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ZB.MOM.WW.OtOpcUa.Cluster.csproj index 942f1bd..234de21 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ZB.MOM.WW.OtOpcUa.Cluster.csproj +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ZB.MOM.WW.OtOpcUa.Cluster.csproj @@ -19,6 +19,10 @@ + + + +