# 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 ServiceCollectionExtensions.WithOtOpcUaClusterBootstrap (via Akka.Hosting). # Everything else here is the cluster-wide tuning that should match across nodes. # # Any divergence from these defaults must be deliberate and recorded in docs/v2/Architecture.md. akka { # Akka logger wiring (route ILoggingAdapter → Serilog) is configured via Akka.Hosting's # ConfigureLoggers in ServiceCollectionExtensions.WithOtOpcUaClusterBootstrap — HOCON # `akka.loggers` alone is not honored by Akka.Hosting. logger-startup-timeout is kept here # since the Serilog logger can be slow to initialize at startup. logger-startup-timeout = 30s 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 # DOWNING. The provider is NOT selected here — this file is added with HoconAddMode.Append, # which loses to what Akka.Hosting's WithClustering emits, so a downing-provider-class set # here would be silently overridden. Selection lives in # ServiceCollectionExtensions.BuildDowningHocon (Prepended, which outranks WithClustering), # driven by Cluster:SplitBrainResolverStrategy. # # The DEFAULT strategy is "auto-down", under which this whole block is inert: Akka's # AutoDowning provider is installed with auto-down-unreachable-after = 15s, and the leader # among the reachable members downs the unreachable peer — so a hard crash of EITHER node, # oldest included, fails over. The trade is dual-active during a real network partition. # # The block below applies only when an operator selects "keep-oldest". Note that keep-oldest # is NOT safe for a two-node pair: Akka.NET's KeepOldest.OldestDecision only lets # down-if-alone rescue a side holding >= 2 members, so a 1-vs-1 survivor downs ITSELF and # (with run-coordinated-shutdown-when-down) exits — a crash of the oldest node becomes a # total outage. See docs/Redundancy.md. # # stable-after must stay >= failure-detector.acceptable-heartbeat-pause (10s) + margin, and # must equal ServiceCollectionExtensions.DowningStableAfter so both strategies fail over at # the same latency. Both constraints are pinned by SplitBrainResolverActivationTests. 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 }