namespace ZB.MOM.WW.OtOpcUa.Cluster; public sealed class AkkaClusterOptions { public const string SectionName = "Cluster"; /// Gets or sets the Akka system name. public string SystemName { get; set; } = "otopcua"; /// Gets or sets the hostname to bind to (default 0.0.0.0). public string Hostname { get; set; } = "0.0.0.0"; /// Gets or sets the port to listen on (default 4053). public int Port { get; set; } = 4053; /// /// Hostname advertised in cluster gossip. Must be reachable by other nodes. /// In docker-compose this is the container DNS name; in bare metal it's the /// host's stable LAN address. /// public string PublicHostname { get; set; } = "127.0.0.1"; /// /// Gets or sets the seed nodes for cluster bootstrapping. /// /// /// /// ORDER IS LOAD-BEARING (decision 2026-07-22): a node that is one of its own seeds /// must list ITSELF first. Akka runs FirstSeedNodeProcess — the only bootstrap /// path that can form a NEW cluster when no peer answers InitJoin — exclusively /// when seed-nodes[0] is this node's own address; any other node runs /// JoinSeedNodeProcess, which retries InitJoin forever and can never form a /// cluster. So listing both peers does NOT mean either can cold-start alone: a node that /// lists its partner first never comes Up while that partner is down. Self-first ordering /// closes that gap inside Akka's own handshake — unlike the retired /// Cluster:SelfFormAfter watchdog, which sat outside it and could not tell "no /// seed answered" from "a seed answered and the join is in flight". /// /// /// The rule is conditional: it binds only when this node's own address appears in /// this list. A node seeded exclusively by someone else — today's docker-dev site nodes, /// which list only central-1 — is legitimately not a seed and is exempt. Enforced /// at boot by ; identity is /// + (what Akka puts in /// SelfAddress), never the bind address. /// /// public string[] SeedNodes { get; set; } = Array.Empty(); /// /// Cluster roles for this node. When empty the role list comes from /// OTOPCUA_ROLES via . Allowed values: /// admin, driver, dev. /// public string[] Roles { get; set; } = Array.Empty(); /// /// How the cluster decides to down a node it can no longer reach. One of auto-down /// (default) or keep-oldest; any other value fails the host at startup. /// /// /// /// auto-down — availability. The leader among the reachable members /// downs the unreachable peer after /// . A hard crash of either /// node — including the oldest — fails over to the survivor with no operator action. /// The trade is that a genuine network partition (both nodes alive, link cut) leaves /// both sides running active until an operator restarts one. /// /// /// keep-oldest — partition-safety. The SBR resolver sacrifices the younger /// side of a split, so a partition can never run dual-active. The cost is severe for a /// two-node pair: it cannot survive a crash of the oldest node at all. Akka.NET's /// KeepOldest.OldestDecision only lets down-if-alone rescue a side holding /// >= 2 members, so the 1-vs-1 survivor downs itself and shuts down — the /// redundancy pair turns a single-node crash into a total outage. Choose this only for /// clusters of three or more nodes, or where dual-active is genuinely worse than an /// outage. /// /// public string SplitBrainResolverStrategy { get; set; } = "auto-down"; /// /// The simultaneous-cold-start split-brain guard (Cluster:BootstrapGuard). Default OFF — a /// dark switch, so existing deployments and tests keep Akka's config-driven self-first auto-join /// unchanged. See for the decision logic and /// ClusterBootstrapCoordinator for the runtime. /// public ClusterBootstrapGuardOptions BootstrapGuard { get; set; } = new(); } /// /// Configuration for the simultaneous-cold-start split-brain guard. When /// , the node does NOT auto-join from its config seeds; a coordinator picks the /// join order (founder self-first / joiner peer-first) after a reachability probe. See /// . /// public sealed class ClusterBootstrapGuardOptions { /// /// Gets or sets whether the guard is active. Default — Akka auto-joins from /// the config seed list exactly as before. Only meaningful on a node that is one of its own two /// pair seeds; inert everywhere else. /// public bool Enabled { get; set; } /// /// Gets or sets how long the higher-address node probes its partner's Akka endpoint before /// concluding the partner is dead and forming alone. Must comfortably exceed the partner's /// worst-case process-start-to-Akka-bind time so a slow-but-alive partner is never mistaken for a /// dead one (which would re-open the split). Default 25 s. /// public int PartnerProbeSeconds { get; set; } = 25; /// /// Gets or sets the interval between partner reachability probes, in milliseconds. Default 500 ms. /// public int PartnerProbeIntervalMs { get; set; } = 500; /// /// Gets or sets the per-probe TCP connect timeout, in milliseconds. Default 1000 ms. /// public int ProbeConnectTimeoutMs { get; set; } = 1000; }