using Microsoft.Extensions.Configuration; namespace ZB.MOM.WW.OtOpcUa.Runtime; /// /// The definitions of "this node's LocalDb is replicated to a peer" (#603) — one place, two deliberately /// different predicates for the two consumers: the Host's table-registration step /// (LocalDbSetup.ReplicationIsConfigured → : which nodes capture /// changes) and the alarm-historian drain gate (AddAlarmHistorian → : /// which nodes stand down in favour of a peer). Lives in Runtime because Host references Runtime and not /// the reverse. /// /// /// /// Why two predicates (review R1 m3). The two answers cost differently when wrong. Registering a /// node that has no working sync path only costs capture triggers. Gating the drain of a node that /// shares its queue with nobody parks its alarm events behind a Primary that does not hold them — /// undelivered until the node is promoted, and drop-oldest at Capacity meanwhile. So /// registration stays permissive and the gate is strict. Every node the gate treats as sharing also /// captures, so the two never disagree in the harmful direction. /// /// /// Registration: LocalDb:Replication:PeerAddress OR LocalDb:Replication:ApiKey. /// The OR is required: only the dialling half of a pair sets PeerAddress, while the passive /// half carries the shared ApiKey (plus a listen port) and nothing else. Keying on /// PeerAddress alone would strip capture from every passive node. /// /// /// Drain gate: ApiKey AND (PeerAddress OR LocalDb:SyncListenPort > 0). A /// peer holds this node's rows only over a working sync path: a dial (PeerAddress) or a /// listener (SyncListenPort), each authenticated by the key. Both auth directions fail closed, /// so a path without the key shares nothing, and a key with neither path (e.g. a fleet-wide /// secret/env ApiKey on a node with no peer) shares nothing either. Every docker-dev shape /// gives the same answer under both predicates: site-a-1 (PeerAddress + ApiKey + port) and site-a-2 /// (ApiKey + port) are gated; central and site-b (neither) are not. /// /// public static class LocalDbReplicationPolicy { /// Configuration section holding the LocalDb replication options. public const string ReplicationSectionPath = "LocalDb:Replication"; /// The dedicated passive-sync listener port key (same key as LocalDbRegistration.SyncListenPortKey). public const string SyncListenPortKey = "LocalDb:SyncListenPort"; /// Whether this node registers its replicated tables for change capture (table registration). /// The application configuration. /// true when PeerAddress or ApiKey is set under the replication section. public static bool CapturesChanges(IConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration); var section = configuration.GetSection(ReplicationSectionPath); return !string.IsNullOrWhiteSpace(section["PeerAddress"]) || !string.IsNullOrWhiteSpace(section["ApiKey"]); } /// Whether a pair peer actually holds this node's replicated rows (the alarm drain gate). /// The application configuration. /// true when ApiKey is set AND either PeerAddress is set or /// LocalDb:SyncListenPort is a positive port. public static bool SharesRowsWithPeer(IConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration); var section = configuration.GetSection(ReplicationSectionPath); if (string.IsNullOrWhiteSpace(section["ApiKey"])) return false; var dials = !string.IsNullOrWhiteSpace(section["PeerAddress"]); var listens = int.TryParse(configuration[SyncListenPortKey], out var port) && port > 0; return dials || listens; } }