fix(controlplane): case-insensitive NodeId equality for deploy ack-set

Aligns ConfigPublishCoordinator's _acks/_expectedAcks with the case-insensitive
ClusterId/NodeId scoping in DeploymentArtifact.ResolveClusterScope, so an ack
from a node whose host:port differs only in case still matches its expected-ack
entry (SQL collation + DNS are case-insensitive).
This commit is contained in:
Joseph Doherty
2026-06-07 08:08:12 -04:00
parent b45e0be427
commit 1f76eac97a
@@ -30,10 +30,15 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers
private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
private readonly TimeSpan _applyDeadline;
private readonly ILoggingAdapter _log = Context.GetLogger();
private readonly Dictionary<NodeId, ApplyAckOutcome> _acks = new();
// NodeId equality here is case-insensitive (by Value) to match the case-insensitive ClusterId/
// NodeId scoping in DeploymentArtifact.ResolveClusterScope — so an ack from a node whose address
// differs only in case still matches its expected-ack entry (SQL collation + DNS are
// case-insensitive, so the same node can surface with different casing).
private static readonly IEqualityComparer<NodeId> NodeIdComparer = new CaseInsensitiveNodeIdComparer();
private readonly Dictionary<NodeId, ApplyAckOutcome> _acks = new(NodeIdComparer);
private DeploymentId? _current;
private HashSet<NodeId> _expectedAcks = new();
private HashSet<NodeId> _expectedAcks = new(NodeIdComparer);
/// <summary>Gets the timer scheduler for managing apply deadlines.</summary>
public ITimerScheduler Timers { get; set; } = null!;
@@ -88,7 +93,7 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers
.AsNoTracking()
.ToList();
_expectedAcks = nodeStates.Select(s => NodeId.Parse(s.NodeId)).ToHashSet();
_expectedAcks = nodeStates.Select(s => NodeId.Parse(s.NodeId)).ToHashSet(NodeIdComparer);
foreach (var s in nodeStates.Where(s => s.Status != NodeDeploymentStatus.Applying))
_acks[NodeId.Parse(s.NodeId)] = s.Status == NodeDeploymentStatus.Applied
? ApplyAckOutcome.Applied
@@ -248,7 +253,7 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers
private HashSet<NodeId> DiscoverDriverNodes()
{
var cluster = Akka.Cluster.Cluster.Get(Context.System);
var nodes = new HashSet<NodeId>();
var nodes = new HashSet<NodeId>(NodeIdComparer);
foreach (var member in cluster.State.Members)
{
if (member.Status is not (MemberStatus.Up or MemberStatus.Joining)) continue;
@@ -261,4 +266,18 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers
}
return nodes;
}
/// <summary>Case-insensitive <see cref="NodeId"/> equality (by <see cref="NodeId.Value"/>),
/// matching the case-insensitive scoping in <c>DeploymentArtifact.ResolveClusterScope</c> so the
/// expected-ack set and incoming acks agree regardless of host-name casing.</summary>
private sealed class CaseInsensitiveNodeIdComparer : IEqualityComparer<NodeId>
{
/// <inheritdoc />
public bool Equals(NodeId x, NodeId y) =>
string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc />
public int GetHashCode(NodeId obj) =>
StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value ?? string.Empty);
}
}