refactor(configdb): drop ClusterNode.RedundancyRole (replaced by Akka leader)

Phase 1d of the v2 entity-model rewrite. The static RedundancyRole column
is replaced by Akka cluster's role-leader-of-"driver" election at runtime
(see RedundancyStateActor + ServiceLevelCalculator in Task 35).

Changes:

  - Removed `public required RedundancyRole RedundancyRole` from
    ClusterNode entity.
  - Removed `e.Property(x => x.RedundancyRole).HasConversion<string>()...`
    mapping from OtOpcUaConfigDbContext.ConfigureClusterNode.
  - Removed the `UX_ClusterNode_Primary_Per_Cluster` filtered unique index
    (filter referenced [RedundancyRole]='Primary').
  - Dropped `using ZB.MOM.WW.OtOpcUa.Configuration.Enums` from ClusterNode.cs
    (no longer needed).
  - Deleted `Enums/RedundancyRole.cs` — the enum is unused in v2-kept code.
  - DraftValidator: dropped the "exactly one Primary per cluster"
    validation block. Comment in place explaining v2 picks primary at
    runtime via Akka.
  - DraftValidatorTests: dropped ValidateClusterTopology_flags_multiple_Primary
    test; reworked BuildNode helper to no longer take a `role` argument.

Untouched (Server + Admin still reference RedundancyRole; accepted broken
per Task 56 policy):

  src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/{ClusterTopologyLoader,
    RedundancyStatePublisher, RedundancyTopology, ServiceLevelCalculator}.cs
  src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/RedundancyMetrics.cs

DB-runtime tests will fail against the new schema (Task 14f's migration
drops the column) — to be updated in Task 14f's SchemaComplianceTests
update:

  - SchemaComplianceTests.cs:55 (expected filtered index list)
  - StoredProceduresTests.cs:263 (raw INSERT names the column)

Verification:
  src/Core/ZB.MOM.WW.OtOpcUa.Configuration            -> 0 errors
  tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests    -> 0 errors
  whole solution                                       -> 71 errors
    (70 from Task 14b in Server/Admin, +1 new Server/Redundancy reference)
This commit is contained in:
Joseph Doherty
2026-05-26 04:11:57 -04:00
parent 1ddf8bb50e
commit 3c915e652e
5 changed files with 16 additions and 45 deletions

View File

@@ -1,5 +1,3 @@
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>Physical OPC UA server node within a <see cref="ServerCluster"/>.</summary>
@@ -10,8 +8,6 @@ public sealed class ClusterNode
public required string ClusterId { get; set; }
public required RedundancyRole RedundancyRole { get; set; }
/// <summary>Machine hostname / IP.</summary>
public required string Host { get; set; }

View File

@@ -1,9 +0,0 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
/// <summary>Per-node redundancy role within a cluster. Per decision #84.</summary>
public enum RedundancyRole
{
Primary,
Secondary,
Standalone,
}

View File

@@ -115,7 +115,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
e.HasKey(x => x.NodeId);
e.Property(x => x.NodeId).HasMaxLength(64);
e.Property(x => x.ClusterId).HasMaxLength(64);
e.Property(x => x.RedundancyRole).HasConversion<string>().HasMaxLength(16);
e.Property(x => x.Host).HasMaxLength(255);
e.Property(x => x.ApplicationUri).HasMaxLength(256);
e.Property(x => x.DriverConfigOverridesJson).HasColumnType("nvarchar(max)");
@@ -130,10 +129,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
// Fleet-wide unique per decision #86
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
// At most one Primary per cluster
e.HasIndex(x => x.ClusterId).IsUnique()
.HasFilter("[RedundancyRole] = 'Primary'")
.HasDatabaseName("UX_ClusterNode_Primary_Per_Cluster");
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator,
// Task 35).
});
}

View File

@@ -228,14 +228,9 @@ public static class DraftValidator
$"Toggle the missing node(s) back on or change RedundancyMode/NodeCount to match.",
cluster.ClusterId));
// Primary uniqueness — decision #84. Two Primary nodes is always an invariant violation
// regardless of mode; catch it here so publish fails loud rather than the runtime
// demoting both to ServiceLevelBand.InvalidTopology at boot.
var primaryCount = clusterNodes.Count(n => n.Enabled && n.RedundancyRole == RedundancyRole.Primary);
if (primaryCount > 1)
errors.Add(new("ClusterMultiplePrimary",
$"Cluster '{cluster.ClusterId}' has {primaryCount} Enabled Primary nodes. At most one Primary per cluster.",
cluster.ClusterId));
// v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was
// dropped in Task 14d; in v2 the Akka cluster's role-leader-of-"driver" elects the
// primary at runtime, so there is no static configuration to validate here.
return errors;
}

View File

@@ -161,7 +161,7 @@ public sealed class DraftValidatorTests
{
var cluster = BuildCluster(nodeCount: nodeCount, mode: mode);
var nodes = Enumerable.Range(0, enabledNodes)
.Select(i => BuildNode($"n-{i}", enabled: true, role: i == 0 ? RedundancyRole.Primary : RedundancyRole.Secondary))
.Select(i => BuildNode($"n-{i}", enabled: true))
.ToList();
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
@@ -175,33 +175,24 @@ public sealed class DraftValidatorTests
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
var nodes = new[]
{
BuildNode("primary", enabled: true, role: RedundancyRole.Primary),
BuildNode("backup", enabled: false, role: RedundancyRole.Secondary),
BuildNode("primary", enabled: true),
BuildNode("backup", enabled: false),
};
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
errors.ShouldContain(e => e.Code == "ClusterEnabledNodeCountMismatch");
}
[Fact]
public void ValidateClusterTopology_flags_multiple_Primary()
{
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
var nodes = new[]
{
BuildNode("a", enabled: true, role: RedundancyRole.Primary),
BuildNode("b", enabled: true, role: RedundancyRole.Primary),
};
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
errors.ShouldContain(e => e.Code == "ClusterMultiplePrimary");
}
// v2: the "exactly one Primary per cluster" check is gone — Akka cluster's
// role-leader-of-"driver" elects the primary at runtime. The corresponding
// ValidateClusterTopology_flags_multiple_Primary test (and the
// ClusterMultiplePrimary error code it asserted) were removed alongside Task 14d.
[Fact]
public void ValidateClusterTopology_returns_no_errors_on_valid_standalone()
{
var cluster = BuildCluster(nodeCount: 1, mode: RedundancyMode.None);
var nodes = new[] { BuildNode("only", enabled: true, role: RedundancyRole.Primary) };
var nodes = new[] { BuildNode("only", enabled: true) };
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
errors.ShouldBeEmpty();
@@ -219,11 +210,10 @@ public sealed class DraftValidatorTests
CreatedBy = "t",
};
private static ClusterNode BuildNode(string id, bool enabled, RedundancyRole role) => new()
private static ClusterNode BuildNode(string id, bool enabled) => new()
{
NodeId = id,
ClusterId = "c-test",
RedundancyRole = role,
Host = "localhost",
OpcUaPort = 4840,
DashboardPort = 5001,