feat(runtime): OPC UA rebuild materialises only the node's ClusterId slice
This commit is contained in:
@@ -209,7 +209,9 @@ public sealed class OpcUaPublishActor : ReceiveActor
|
|||||||
var artifact = msg.DeploymentId is { } depId
|
var artifact = msg.DeploymentId is { } depId
|
||||||
? LoadArtifact(depId)
|
? LoadArtifact(depId)
|
||||||
: LoadLatestArtifact();
|
: LoadLatestArtifact();
|
||||||
var composition = DeploymentArtifact.ParseComposition(artifact);
|
var composition = _localNode is { } ln
|
||||||
|
? DeploymentArtifact.ParseComposition(artifact, ln.Value)
|
||||||
|
: DeploymentArtifact.ParseComposition(artifact);
|
||||||
var plan = Phase7Planner.Compute(_lastApplied, composition);
|
var plan = Phase7Planner.Compute(_lastApplied, composition);
|
||||||
|
|
||||||
if (plan.IsEmpty)
|
if (plan.IsEmpty)
|
||||||
|
|||||||
@@ -98,6 +98,104 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
|||||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wiring proof for per-ClusterId scoping (Task 4): a multi-cluster artifact must
|
||||||
|
/// materialise ONLY the local node's cluster slice. Mirrors the multi-cluster artifact
|
||||||
|
/// shape exercised in <c>DeploymentArtifactTests</c> (MAIN + SITE-A, one Galaxy driver +
|
||||||
|
/// one SystemPlatform tag each). The scoped rebuild for the SITE-A node must surface the
|
||||||
|
/// SITE-A tag (<c>t-sa</c> → variable <c>F.S1</c>) and NOT MAIN's (<c>t-main</c> →
|
||||||
|
/// <c>F.M1</c>); the mirror holds for the MAIN node. Without the production scoping edit,
|
||||||
|
/// the unscoped parse would materialise BOTH variables on every node.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Rebuild_materialises_only_the_nodes_cluster()
|
||||||
|
{
|
||||||
|
// --- SITE-A node: only the SITE-A tag's variable, never MAIN's. ---
|
||||||
|
var dbA = NewInMemoryDbFactory();
|
||||||
|
var sinkA = new RecordingSink();
|
||||||
|
var applierA = new Phase7Applier(sinkA, NullLogger<Phase7Applier>.Instance);
|
||||||
|
SeedMultiClusterDeployment(dbA);
|
||||||
|
|
||||||
|
var siteActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
|
||||||
|
sink: sinkA,
|
||||||
|
dbFactory: dbA,
|
||||||
|
applier: applierA,
|
||||||
|
localNode: NodeId.Parse("site-a-1:4053")));
|
||||||
|
|
||||||
|
siteActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||||
|
|
||||||
|
AwaitAssert(() => sinkA.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
|
||||||
|
// t-sa (Name "S1", FolderPath "F") → MxAccessRef "F.S1" → variable node "F.S1".
|
||||||
|
sinkA.Calls.ShouldContain("EV:F.S1");
|
||||||
|
// t-main (MAIN cluster) must NOT leak onto the SITE-A node.
|
||||||
|
sinkA.Calls.ShouldNotContain("EV:F.M1");
|
||||||
|
|
||||||
|
// --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. ---
|
||||||
|
var dbM = NewInMemoryDbFactory();
|
||||||
|
var sinkM = new RecordingSink();
|
||||||
|
var applierM = new Phase7Applier(sinkM, NullLogger<Phase7Applier>.Instance);
|
||||||
|
SeedMultiClusterDeployment(dbM);
|
||||||
|
|
||||||
|
var mainActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
|
||||||
|
sink: sinkM,
|
||||||
|
dbFactory: dbM,
|
||||||
|
applier: applierM,
|
||||||
|
localNode: NodeId.Parse("central-1:4053")));
|
||||||
|
|
||||||
|
mainActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||||
|
|
||||||
|
AwaitAssert(() => sinkM.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
|
||||||
|
sinkM.Calls.ShouldContain("EV:F.M1");
|
||||||
|
sinkM.Calls.ShouldNotContain("EV:F.S1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seal a 2-cluster deployment (MAIN + SITE-A) whose artifact mirrors the multi-cluster
|
||||||
|
/// shape the composer emits: a <c>Clusters</c> + <c>Nodes</c> map, one SystemPlatform
|
||||||
|
/// namespace + Galaxy driver + Galaxy tag per cluster. Used by
|
||||||
|
/// <see cref="Rebuild_materialises_only_the_nodes_cluster"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static void SeedMultiClusterDeployment(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory)
|
||||||
|
{
|
||||||
|
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
||||||
|
{
|
||||||
|
Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
|
||||||
|
Nodes = new[]
|
||||||
|
{
|
||||||
|
new { NodeId = "central-1:4053", ClusterId = "MAIN" },
|
||||||
|
new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
|
||||||
|
},
|
||||||
|
DriverInstances = new[]
|
||||||
|
{
|
||||||
|
new { DriverInstanceId = "main-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
|
||||||
|
new { DriverInstanceId = "sa-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
|
||||||
|
},
|
||||||
|
Namespaces = new[]
|
||||||
|
{
|
||||||
|
new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 1 }, // NamespaceKind.SystemPlatform
|
||||||
|
new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 1 },
|
||||||
|
},
|
||||||
|
Tags = new[]
|
||||||
|
{
|
||||||
|
new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)null, Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
|
||||||
|
new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)null, Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
|
||||||
|
},
|
||||||
|
ScriptedAlarms = Array.Empty<object>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
using var ctx = dbFactory.CreateDbContext();
|
||||||
|
ctx.Deployments.Add(new Deployment
|
||||||
|
{
|
||||||
|
DeploymentId = Guid.NewGuid(),
|
||||||
|
RevisionHash = new string('b', 64),
|
||||||
|
Status = DeploymentStatus.Sealed,
|
||||||
|
CreatedBy = "test",
|
||||||
|
SealedAtUtc = DateTime.UtcNow,
|
||||||
|
ArtifactBlob = artifact,
|
||||||
|
});
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
private static void SeedDeployment(
|
private static void SeedDeployment(
|
||||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||||
string[] equipmentIds,
|
string[] equipmentIds,
|
||||||
|
|||||||
Reference in New Issue
Block a user