review(ControlPlane): fix premature deploy-seal from unexpected-node ack

Review at HEAD 7286d320. ControlPlane-001 (Medium): ConfigPublishCoordinator.HandleAck
now discards acks from nodes not in _expectedAcks (prevented premature SealDeployment) +
regression test. -002 (flipped-node log count), -003 (redundant mapper arms) tidied.
This commit is contained in:
Joseph Doherty
2026-06-19 10:52:22 -04:00
parent 3512089c90
commit 1aa7905676
5 changed files with 181 additions and 13 deletions
@@ -29,16 +29,10 @@ public static class AuditOutcomeMapper
public static AuditOutcome FromAction(string action) => action switch
{
"OpcUaAccessDenied" or "CrossClusterNamespaceAttempt" => AuditOutcome.Denied,
"DraftCreated"
or "DraftEdited"
or "Published"
or "RolledBack"
or "NodeApplied"
or "ClusterCreated"
or "NodeAdded"
or "CredentialAdded"
or "CredentialDisabled"
or "ExternalIdReleased" => AuditOutcome.Success,
// All other known config-write verbs (DraftCreated, DraftEdited, Published, RolledBack,
// NodeApplied, ClusterCreated, NodeAdded, CredentialAdded, CredentialDisabled,
// ExternalIdReleased) and any future verbs default to Success — config writes are the
// overwhelming majority and the only non-success cases are the two Denied entries above.
_ => AuditOutcome.Success,
};
}
@@ -160,6 +160,16 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers
return;
}
// Discard acks from nodes that were not in the expected-ack set at dispatch time.
// Without this guard an unexpected-node ack inflates _acks.Count and can trigger a
// premature seal/fail before every expected node has actually applied the deployment.
if (!_expectedAcks.Contains(msg.NodeId))
{
_log.Debug("Discarding ApplyAck from unexpected node {Node} for {Id} (not in expected set)",
msg.NodeId, msg.DeploymentId);
return;
}
_acks[msg.NodeId] = msg.Outcome;
PersistNodeAck(msg);
@@ -127,16 +127,16 @@ public sealed class FleetStatusBroadcaster : ReceiveActor, IWithTimers
private void OnTick()
{
var stale = DateTime.UtcNow - HeartbeatTimeout;
var changed = false;
var flippedCount = 0;
foreach (var kv in _nodes.ToList())
{
if (kv.Value.LastSeenUtc < stale && kv.Value.Health == FleetNodeHealth.Healthy)
{
_nodes[kv.Key] = kv.Value with { Health = FleetNodeHealth.Degraded };
changed = true;
flippedCount++;
}
}
if (changed) _log.Debug("FleetStatusBroadcaster flipped {Count} nodes to Degraded", changed ? 1 : 0);
if (flippedCount > 0) _log.Debug("FleetStatusBroadcaster flipped {Count} nodes to Degraded", flippedCount);
PublishSnapshot();
}