diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs index e6cf5ce6..c9c36104 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs @@ -30,10 +30,15 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers private readonly IDbContextFactory _dbFactory; private readonly TimeSpan _applyDeadline; private readonly ILoggingAdapter _log = Context.GetLogger(); - private readonly Dictionary _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 NodeIdComparer = new CaseInsensitiveNodeIdComparer(); + private readonly Dictionary _acks = new(NodeIdComparer); private DeploymentId? _current; - private HashSet _expectedAcks = new(); + private HashSet _expectedAcks = new(NodeIdComparer); /// Gets the timer scheduler for managing apply deadlines. 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 DiscoverDriverNodes() { var cluster = Akka.Cluster.Cluster.Get(Context.System); - var nodes = new HashSet(); + var nodes = new HashSet(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; } + + /// Case-insensitive equality (by ), + /// matching the case-insensitive scoping in DeploymentArtifact.ResolveClusterScope so the + /// expected-ack set and incoming acks agree regardless of host-name casing. + private sealed class CaseInsensitiveNodeIdComparer : IEqualityComparer + { + /// + public bool Equals(NodeId x, NodeId y) => + string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase); + + /// + public int GetHashCode(NodeId obj) => + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value ?? string.Empty); + } }