Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/LocalDbReplicationPolicy.cs
T
Joseph Doherty 4f6231fa40 refactor(localdb): one replication predicate shared by table registration and the alarm drain gate (#603)
The drain gate counted SyncListenPort; a port with no ApiKey shares nothing (fail-closed
passive endpoint), and an ApiKey-only passive node shares its queue but was never gated.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:56:00 -04:00

44 lines
2.5 KiB
C#

using Microsoft.Extensions.Configuration;
namespace ZB.MOM.WW.OtOpcUa.Runtime;
/// <summary>
/// The ONE definition of "this node's LocalDb is replicated to a peer" (#603). Two consumers must agree
/// on it, and used to carry separate predicates that diverged: the Host's table-registration step
/// (<c>LocalDbSetup.ReplicationIsConfigured</c> - which nodes capture changes) and the alarm-historian
/// drain gate (<c>AddAlarmHistorian</c> - which nodes stand down in favour of a peer). Lives in Runtime
/// because Host references Runtime and not the reverse.
/// </summary>
/// <remarks>
/// <para>
/// <b>Predicate: <c>LocalDb:Replication:PeerAddress</c> OR <c>LocalDb:Replication:ApiKey</c> is
/// non-blank.</b> The OR is required: only the dialling half of a pair sets <c>PeerAddress</c>, while
/// the passive half carries the shared <c>ApiKey</c> (plus a listen port) and nothing else.
/// </para>
/// <para>
/// <b>Why the drain gate takes the same predicate (it previously used PeerAddress OR
/// <c>SyncListenPort</c>).</b> The gate's question is "does a peer hold the same rows?". The passive
/// sync endpoint is fail-closed on the key, so a node with a listen port but no <c>ApiKey</c> rejects
/// every inbound stream and shares its queue with nobody - gating it would park its alarm events
/// behind a Primary that cannot deliver them. Conversely an <c>ApiKey</c>-only passive node IS
/// registered and does receive its peer's rows, so it must be gated or both halves drain. The port
/// is not evidence of a peer; the key is.
/// </para>
/// </remarks>
public static class LocalDbReplicationPolicy
{
/// <summary>Configuration section holding the LocalDb replication options.</summary>
public const string ReplicationSectionPath = "LocalDb:Replication";
/// <summary>Whether this node's LocalDb replicates to a pair peer.</summary>
/// <param name="configuration">The application configuration.</param>
/// <returns><c>true</c> when <c>PeerAddress</c> or <c>ApiKey</c> is set under the replication section.</returns>
public static bool IsConfigured(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var section = configuration.GetSection(ReplicationSectionPath);
return !string.IsNullOrWhiteSpace(section["PeerAddress"])
|| !string.IsNullOrWhiteSpace(section["ApiKey"]);
}
}