namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
///
/// Cluster configuration model, bound from the ScadaBridge:Cluster section
/// of appsettings.json via the Options pattern.
///
/// This project owns the cluster configuration contract. The actual
/// Akka.NET bootstrap — building the HOCON from these values, starting the
/// ActorSystem, configuring the split-brain resolver and wiring
/// CoordinatedShutdown — lives in ZB.MOM.WW.ScadaBridge.Host
/// (see Component-ClusterInfrastructure.md → "Implementation Note — Code Placement").
///
///
/// Node-identity settings (remoting hostname/port, cluster role, site identifier,
/// gRPC port) are deliberately not here — they are owned by
/// ZB.MOM.WW.ScadaBridge.Host.NodeOptions (ScadaBridge:Node section). Local SQLite
/// storage paths are owned by the database / store-and-forward options. This class
/// holds only the cluster-formation and failure-detection settings shared by every node.
///
///
public class ClusterOptions
{
// The previous `public const string SectionName = "ScadaBridge:Cluster";`
// was documented as "single source of truth so binding sites do not hard-code the
// magic string" but no caller ever read it — the Host's SiteServiceRegistration and
// StartupValidator both hard-code the literal directly. Wiring those binding sites
// to reference the constant lives in the Host's edit scope (a separate code-review
// task); rather than carry a public constant whose guarantee the code does not
// deliver, the constant is removed and the literal stays in the Host until the
// Host-side wiring is done. If a future Host change wants the constant back, add it
// when the binding sites can be updated in the same commit.
///
/// Akka.NET cluster seed nodes. Both nodes are seed nodes — each node lists itself and its
/// partner.
///
/// ORDER IS LOAD-BEARING (decision 2026-07-22): every node 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 and retries
/// InitJoin forever. So merely listing both nodes does NOT mean either can start
/// first: a node that lists its partner first can never cold-start while that partner is
/// down (the "registered outage gap", docker/README.md). Self-first ordering closes
/// it using Akka's own protocol, which — unlike an external self-form timer — is part of
/// the join handshake and so cannot mistake an in-flight join for an absent peer.
/// Enforced at boot by StartupValidator.
///
/// Must contain at least one entry.
///
public List SeedNodes { get; set; } = new();
///
/// Downing strategy for unreachable members. Two supported values:
///
/// - auto-down (default, decision 2026-07-21) — availability-first: each
/// side downs the unreachable peer after , so a hard crash
/// of EITHER node (oldest included) fails over to the survivor. The accepted trade:
/// a true network partition produces two live one-node clusters (dual-active) until
/// an operator restarts one side. Chosen because ScadaBridge pairs run one node per
/// VM with no shared lease infrastructure, and a stalled system is a bigger risk
/// than a rare partition.
/// - keep-oldest — partition-safe SBR: downs the side without the oldest
/// member. In a TWO-node cluster this makes a crash of the oldest/active node a
/// total outage: Akka's down-if-alone only rescues the survivor when its own
/// side has ≥2 members (verified against Akka.NET 1.5.62 KeepOldest.Decide
/// and live on the docker rig, 2026-07-21).
///
/// Other SBR strategies are rejected: static-quorum with quorum 1 hits Akka's
/// IsTooManyMembers guard (2 > 2*1-1) and downs ALL on any unreachability;
/// keep-majority just moves the fatal crash from the oldest to the
/// lowest-address node.
///
public string SplitBrainResolverStrategy { get; set; } = "auto-down";
///
/// Time the cluster membership must remain stable before the split-brain
/// resolver acts to down unreachable nodes. Must be positive. Default 15s.
///
public TimeSpan StableAfter { get; set; } = TimeSpan.FromSeconds(15);
///
/// Frequency of cluster failure-detector heartbeat messages between nodes.
/// Must be well below . Default 2s.
///
public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(2);
///
/// Time without a heartbeat before a node is considered unreachable
/// (Akka's acceptable-heartbeat-pause). Default 10s.
///
public TimeSpan FailureDetectionThreshold { get; set; } = TimeSpan.FromSeconds(10);
///
/// Akka's min-nr-of-members. Must be 1: after failover only one
/// node runs, and a value of 2 blocks the cluster singleton (Site Runtime
/// Deployment Manager) — and therefore all data collection — indefinitely.
///
public int MinNrOfMembers { get; set; } = 1;
///
/// The keep-oldest resolver's down-if-alone flag; only consulted when
/// is keep-oldest. When true,
/// the oldest node downs itself if it finds it has no other reachable members,
/// rather than running as an isolated single-node cluster. Note that in a two-node
/// cluster this does NOT let the younger survivor take over from a crashed oldest —
/// Akka's alone-check requires the surviving side to have ≥2 members.
///
public bool DownIfAlone { get; set; } = true;
///
/// Acknowledges a deliberate single-node deployment: permits exactly one seed
/// node (normally ≥2 are required so either node can start first). Without this
/// flag a single-node install must list a phantom second seed, which the node
/// dials forever — log noise and a defeated validation intent (review 01).
///
public bool AllowSingleNodeCluster { get; set; }
///
/// The simultaneous-cold-start split-brain guard (ScadaBridge:Cluster:BootstrapGuard).
/// Default OFF — a dark switch, so existing deployments and tests keep Akka's config-driven
/// self-first auto-join byte-identical. See for the
/// decision logic and ClusterBootstrapCoordinator for the runtime (Gitea #33).
///
public ClusterBootstrapGuardOptions BootstrapGuard { get; set; } = new();
}
///
/// Configuration for the simultaneous-cold-start split-brain guard. Both nodes of a pair are
/// self-first seeds so either can cold-start alone when its partner is dead — but the cost
/// is that when BOTH cold-start at the same instant each runs Akka's FirstSeedNodeProcess,
/// times out waiting for the other, and forms its own single-node cluster (a split brain that does
/// not auto-merge). When , the node does NOT auto-join from its config seeds
/// (AkkaHostedService.BuildHocon emits an empty seed list); a coordinator picks the join
/// order — founder self-first / joiner peer-first — after a reachability probe. See
/// .
///
public sealed class ClusterBootstrapGuardOptions
{
///
/// 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 (single-seed site node, self absent, 3+ seeds).
///
public bool Enabled { get; set; }
///
/// 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 25s. Validated > 0 at boot when the guard is on.
///
public int PartnerProbeSeconds { get; set; } = 25;
///
/// The interval between partner reachability probes, in milliseconds. Default 500ms.
///
public int PartnerProbeIntervalMs { get; set; } = 500;
///
/// The per-probe TCP connect timeout, in milliseconds. Default 1000ms.
///
public int ProbeConnectTimeoutMs { get; set; } = 1000;
}