fix(runtime): the passive half of a LocalDb pair stands its alarm drain down for its mesh partner (review R6 MINOR-4)
The drain gate's 'is my queue-sharing peer Primary?' used only the host of
LocalDb:Replication:PeerAddress, which the passive half (ApiKey +
SyncListenPort) does not set - so as Secondary it kept draining beside its
Primary partner and every alarm event reached the historian twice.
DriverHostActor takes a sharesAlarmQueueWithLocal predicate used when no
peer host is known; WithOtOpcUaRuntimeActors builds it
(BuildAlarmQueuePeerPredicate) for a SharesRowsWithPeer node with no
PeerAddress as 'a member of my own cluster-{ClusterId} mesh other than
me'. A node sharing no rows, or with no mesh role, gets none and keeps
draining. AlarmHistorian.md / Redundancy.md / pair-replication runbook
updated.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -279,6 +279,33 @@ public static class ServiceCollectionExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review R6 MINOR-4 — "does this snapshot node hold my replicated alarm queue?" for a node that does not
|
||||
/// dial its peer. Both halves of a LocalDb pair hold the same rows, but only the dialling half knows its
|
||||
/// partner (<c>LocalDb:Replication:PeerAddress</c>); the passive half (<c>ApiKey</c> + <c>SyncListenPort</c>)
|
||||
/// knew nobody, so as Secondary it never stood its drain down and both nodes delivered every alarm event.
|
||||
/// Its partner is its per-cluster mesh partner, which a cluster-scoped role identifies.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The host configuration (the LocalDb replication keys).</param>
|
||||
/// <param name="roleInfo">This node's cluster role info.</param>
|
||||
/// <param name="replicationPeerHost">The dialled peer's host, when this node dials one.</param>
|
||||
/// <returns>
|
||||
/// <c>null</c> when the node dials its peer (the host match covers it), when it shares its queue with no
|
||||
/// peer (<see cref="LocalDbReplicationPolicy.SharesRowsWithPeer"/> false — it must never stand down), or
|
||||
/// when it carries no cluster-scoped role (a legacy node cannot prove which node is its partner, and
|
||||
/// standing down for the wrong Primary would strand its rows); otherwise "is a member of my own mesh other
|
||||
/// than me".
|
||||
/// </returns>
|
||||
internal static Func<Commons.Types.NodeId, bool>? BuildAlarmQueuePeerPredicate(
|
||||
IConfiguration? configuration, IClusterRoleInfo? roleInfo, string? replicationPeerHost)
|
||||
{
|
||||
if (replicationPeerHost is not null || configuration is null || roleInfo is null) return null;
|
||||
if (!LocalDbReplicationPolicy.SharesRowsWithPeer(configuration)) return null;
|
||||
if (roleInfo.ClusterRole is not { } meshRole) return null;
|
||||
var local = roleInfo.LocalNode;
|
||||
return nodeId => nodeId != local && roleInfo.MembersWithRole(meshRole).Contains(nodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the per-node driver-role actors on the host's <see cref="ActorSystem"/>:
|
||||
/// <see cref="DriverHostActor"/> (one per node), <see cref="DbHealthProbeActor"/>
|
||||
@@ -361,7 +388,8 @@ public static class ServiceCollectionExtensions
|
||||
var redundancyRoleView = resolver.GetService<IRedundancyRoleView>();
|
||||
// Host of this node's LocalDb replication partner: the ONLY node that holds a copy of this
|
||||
// node's alarm queue, and so the only node it may stand down in favour of. Null when this
|
||||
// node dials nobody, which correctly means "never stand down".
|
||||
// node dials nobody — unpaired (never stand down), or the PASSIVE half of a pair, which is
|
||||
// told its partner through BuildAlarmQueuePeerPredicate instead (review R6 MINOR-4).
|
||||
var replicationPeerHost =
|
||||
Uri.TryCreate(
|
||||
resolver.GetService<IConfiguration>()?["LocalDb:Replication:PeerAddress"],
|
||||
@@ -592,7 +620,11 @@ public static class ServiceCollectionExtensions
|
||||
// Gitea #537: where this actor publishes the continuous-historization CAPTURE
|
||||
// verdict, and how it decides which peer is worth standing down for.
|
||||
historizationRoleView: resolver.GetService<IHistorizationRoleView>(),
|
||||
sharesHistorizationScopeWithLocal: historizationScopePeer),
|
||||
sharesHistorizationScopeWithLocal: historizationScopePeer,
|
||||
// Review R6 MINOR-4: the passive half of a LocalDb pair has no PeerAddress, so it is told
|
||||
// its queue-sharing peer as its mesh partner instead — otherwise it never stands down.
|
||||
sharesAlarmQueueWithLocal: BuildAlarmQueuePeerPredicate(
|
||||
resolver.GetService<IConfiguration>(), roleInfo, replicationPeerHost)),
|
||||
DriverHostActorName);
|
||||
registry.Register<DriverHostActorKey>(driverHost);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user