fix(runtime): pin the Primary-verdict member-event subscription with a test; document the pre-join allow window (review R6 MINOR-2/3)

MINOR-3: new DriverHostActorPrimaryRoleSourceTests case drives the
deny flip with a real ClusterEvent.MemberUp on the EventStream (how the
cluster's domain-event publisher reaches a Cluster.Subscribe subscriber)
instead of a direct RepublishPrimaryVerdict tell; it fails when the
Cluster.Subscribe(IMemberEvent) call is deleted.

MINOR-2: the published verdict MIRRORS the live gate; before this node has
joined its member count is 0 and both allow (seconds; up to the bootstrap
guard's 25 s probe), so the Sparkplug primary-host claim can be held on
both nodes in that window (native ack is unaffected - view and route
agree). Behaviour deliberately unchanged: denying until self is Up would
change the live gate too and delay writes/acks on a single-node cluster's
own join. Comments + AlarmTracking.md / Redundancy.md / Mqtt.md corrected.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 08:33:52 -04:00
parent 52708bff52
commit e93d115cbe
5 changed files with 52 additions and 4 deletions
@@ -802,6 +802,19 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
/// membership changes (a driver peer coming Up turns an unknown role from allow into deny before any
/// snapshot names this node). A non-cluster ActorRefProvider (legacy/test harnesses) just keeps the
/// initial publish.
/// <para>
/// ⚠️ <b>It MIRRORS the live gate; it does not deny from host start.</b> The host is spawned at
/// ActorSystem start, before this node has joined, when its view of <c>Cluster.State.Members</c> is
/// empty — a member count of 0, which the single-node posture ALLOWS. So the verdict published here is
/// allow, and it flips to deny only once this node's own cluster state shows a second Up driver member:
/// normally seconds, up to <c>Cluster:BootstrapGuard:PartnerProbeSeconds</c> (25 s) under the
/// bootstrap guard. In that pre-join window the Sparkplug primary-host claim can be held on both nodes of
/// a pair. Native ack is NOT exposed by it: the live route gate reads the same count and also allows, so
/// the view and the route still agree and the acked → unacked revert cannot occur (review R6 MINOR-2).
/// Denying until self is Up was considered and NOT done: it would change the live data-plane gate (the
/// two must stay one decision) and so delay writes and acks on a genuinely single-node cluster during its
/// own join, and this actor has no reliable way to know that a partner is configured.
/// </para>
/// </summary>
private void StartPrimaryVerdictPublishing()
{