fix(code-review): resolve Batch 3 wave A (OpcUaServer history/guard, ControlPlane topology gate)
- OpcUaServer-002: HistoryRead-Events NumValuesPerNode==0 now maps to unbounded (int.MaxValue) instead of the backend default-cap sentinel; no Core.Abstractions contract change (+EventMaxEvents helper tests) - OpcUaServer-004: EnsureAddressSpaceCreated guard on public mutators -> clear InvalidOperationException instead of bare NRE if called pre-start (+tests) - OpcUaServer-003: Deferred (endUtc inclusive/exclusive needs live Wonderware boundary confirmation) - Configuration-013: wire DraftValidator.ValidateClusterTopology into AdminOperationsActor deploy gate (read-only, no migration) (+2 tests)
This commit is contained in:
@@ -173,7 +173,35 @@ public sealed class AdminOperationsActor : ReceiveActor
|
||||
// committed/visible when the snapshot is read — operators seeing a spurious one should
|
||||
// check ExternalIdReservation state before re-submitting.
|
||||
var draft = await DraftSnapshotFactory.FromConfigDbAsync(db);
|
||||
var errors = DraftValidator.Validate(draft);
|
||||
var errors = DraftValidator.Validate(draft).ToList();
|
||||
|
||||
// Cluster-topology guard (decision #91 / task #148 part 2). The SQL
|
||||
// CK_ServerCluster_RedundancyMode_NodeCount CHECK enforces the (NodeCount, RedundancyMode)
|
||||
// pair on the row itself, but it cannot see the per-node ClusterNode.Enabled flag — an
|
||||
// operator can disable a node (effective enabled-count = 1) while leaving RedundancyMode at
|
||||
// Hot/Warm and the constraint stays green, which would boot the runtime into an
|
||||
// InvalidTopology band. ValidateClusterTopology catches that drift, but it isn't carried on
|
||||
// the generation-versioned DraftSnapshot (the cluster/node rows aren't versioned), so it must
|
||||
// be run separately here against the live rows. Read-only (AsNoTracking); errors fold into the
|
||||
// same reject summary alongside the snapshot rules so a deploy failing either check is
|
||||
// rejected with both sets of messages. ClusterId-ordered for a deterministic summary.
|
||||
var clusters = await db.ServerClusters
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.ClusterId)
|
||||
.ToListAsync();
|
||||
var nodesByCluster = (await db.ClusterNodes
|
||||
.AsNoTracking()
|
||||
.ToListAsync())
|
||||
.GroupBy(n => n.ClusterId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||
foreach (var cluster in clusters)
|
||||
{
|
||||
var nodes = nodesByCluster.TryGetValue(cluster.ClusterId, out var ns)
|
||||
? (IReadOnlyList<ClusterNode>)ns
|
||||
: [];
|
||||
errors.AddRange(DraftValidator.ValidateClusterTopology(cluster, nodes));
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var summary = string.Join("; ", errors.Select(e => $"[{e.Code}] {e.Message}"));
|
||||
|
||||
Reference in New Issue
Block a user