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:
@@ -1,5 +1,3 @@
|
|||||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>Physical OPC UA server node within a <see cref="ServerCluster"/>.</summary>
|
/// <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 string ClusterId { get; set; }
|
||||||
|
|
||||||
public required RedundancyRole RedundancyRole { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Machine hostname / IP.</summary>
|
/// <summary>Machine hostname / IP.</summary>
|
||||||
public required string Host { get; set; }
|
public required string Host { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -115,7 +115,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
e.HasKey(x => x.NodeId);
|
e.HasKey(x => x.NodeId);
|
||||||
e.Property(x => x.NodeId).HasMaxLength(64);
|
e.Property(x => x.NodeId).HasMaxLength(64);
|
||||||
e.Property(x => x.ClusterId).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.Host).HasMaxLength(255);
|
||||||
e.Property(x => x.ApplicationUri).HasMaxLength(256);
|
e.Property(x => x.ApplicationUri).HasMaxLength(256);
|
||||||
e.Property(x => x.DriverConfigOverridesJson).HasColumnType("nvarchar(max)");
|
e.Property(x => x.DriverConfigOverridesJson).HasColumnType("nvarchar(max)");
|
||||||
@@ -130,10 +129,10 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
// Fleet-wide unique per decision #86
|
// Fleet-wide unique per decision #86
|
||||||
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
||||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
||||||
// At most one Primary per cluster
|
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
|
||||||
e.HasIndex(x => x.ClusterId).IsUnique()
|
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
|
||||||
.HasFilter("[RedundancyRole] = 'Primary'")
|
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator,
|
||||||
.HasDatabaseName("UX_ClusterNode_Primary_Per_Cluster");
|
// Task 35).
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,14 +228,9 @@ public static class DraftValidator
|
|||||||
$"Toggle the missing node(s) back on or change RedundancyMode/NodeCount to match.",
|
$"Toggle the missing node(s) back on or change RedundancyMode/NodeCount to match.",
|
||||||
cluster.ClusterId));
|
cluster.ClusterId));
|
||||||
|
|
||||||
// Primary uniqueness — decision #84. Two Primary nodes is always an invariant violation
|
// v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was
|
||||||
// regardless of mode; catch it here so publish fails loud rather than the runtime
|
// dropped in Task 14d; in v2 the Akka cluster's role-leader-of-"driver" elects the
|
||||||
// demoting both to ServiceLevelBand.InvalidTopology at boot.
|
// primary at runtime, so there is no static configuration to validate here.
|
||||||
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));
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ public sealed class DraftValidatorTests
|
|||||||
{
|
{
|
||||||
var cluster = BuildCluster(nodeCount: nodeCount, mode: mode);
|
var cluster = BuildCluster(nodeCount: nodeCount, mode: mode);
|
||||||
var nodes = Enumerable.Range(0, enabledNodes)
|
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();
|
.ToList();
|
||||||
|
|
||||||
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
|
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
|
||||||
@@ -175,33 +175,24 @@ public sealed class DraftValidatorTests
|
|||||||
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
|
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
|
||||||
var nodes = new[]
|
var nodes = new[]
|
||||||
{
|
{
|
||||||
BuildNode("primary", enabled: true, role: RedundancyRole.Primary),
|
BuildNode("primary", enabled: true),
|
||||||
BuildNode("backup", enabled: false, role: RedundancyRole.Secondary),
|
BuildNode("backup", enabled: false),
|
||||||
};
|
};
|
||||||
|
|
||||||
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
|
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
|
||||||
errors.ShouldContain(e => e.Code == "ClusterEnabledNodeCountMismatch");
|
errors.ShouldContain(e => e.Code == "ClusterEnabledNodeCountMismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
// v2: the "exactly one Primary per cluster" check is gone — Akka cluster's
|
||||||
public void ValidateClusterTopology_flags_multiple_Primary()
|
// role-leader-of-"driver" elects the primary at runtime. The corresponding
|
||||||
{
|
// ValidateClusterTopology_flags_multiple_Primary test (and the
|
||||||
var cluster = BuildCluster(nodeCount: 2, mode: RedundancyMode.Hot);
|
// ClusterMultiplePrimary error code it asserted) were removed alongside Task 14d.
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ValidateClusterTopology_returns_no_errors_on_valid_standalone()
|
public void ValidateClusterTopology_returns_no_errors_on_valid_standalone()
|
||||||
{
|
{
|
||||||
var cluster = BuildCluster(nodeCount: 1, mode: RedundancyMode.None);
|
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);
|
var errors = DraftValidator.ValidateClusterTopology(cluster, nodes);
|
||||||
errors.ShouldBeEmpty();
|
errors.ShouldBeEmpty();
|
||||||
@@ -219,11 +210,10 @@ public sealed class DraftValidatorTests
|
|||||||
CreatedBy = "t",
|
CreatedBy = "t",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static ClusterNode BuildNode(string id, bool enabled, RedundancyRole role) => new()
|
private static ClusterNode BuildNode(string id, bool enabled) => new()
|
||||||
{
|
{
|
||||||
NodeId = id,
|
NodeId = id,
|
||||||
ClusterId = "c-test",
|
ClusterId = "c-test",
|
||||||
RedundancyRole = role,
|
|
||||||
Host = "localhost",
|
Host = "localhost",
|
||||||
OpcUaPort = 4840,
|
OpcUaPort = 4840,
|
||||||
DashboardPort = 5001,
|
DashboardPort = 5001,
|
||||||
|
|||||||
Reference in New Issue
Block a user