perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
@@ -18,7 +18,10 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
using ZB.MOM.WW.ScadaBridge.ManagementService;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||
@@ -242,13 +245,14 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
[Fact]
|
||||
public void ListTemplatesCommand_ReturnsTemplateData()
|
||||
{
|
||||
var templates = new List<Template>
|
||||
{
|
||||
new("PumpTemplate") { Id = 1, Description = "Pump" },
|
||||
new("ValveTemplate") { Id = 2, Description = "Valve" }
|
||||
};
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(templates);
|
||||
// ListTemplates now reads DB-side row summaries rather than materialising
|
||||
// every template's full child graph and paging it in memory.
|
||||
_templateRepo.GetTemplateSummariesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new List<TemplateSummary>
|
||||
{
|
||||
new(1, "PumpTemplate", "Pump", null, null, false, null, 0, 0, 0, 0, 0),
|
||||
new(2, "ValveTemplate", "Valve", null, null, false, null, 0, 0, 0, 0, 0)
|
||||
});
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new ListTemplatesCommand());
|
||||
@@ -343,7 +347,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
[Fact]
|
||||
public void ListTemplatesCommand_WhenRepoThrows_ReturnsManagementError()
|
||||
{
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
_templateRepo.GetTemplateSummariesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("Database connection lost"));
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1138,6 +1142,74 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
private static Commons.Entities.Deployment.DeploymentRecord DeploymentRecordFor(int instanceId) =>
|
||||
new("deploy-" + instanceId, "operator") { Id = instanceId, InstanceId = instanceId };
|
||||
|
||||
/// <summary>
|
||||
/// Row-summary equivalent of <see cref="DeploymentRecordFor"/>. QueryDeployments
|
||||
/// now returns DB-side projections instead of whole tracked entities.
|
||||
/// </summary>
|
||||
private static DeploymentRecordSummary DeploymentSummaryFor(int instanceId) =>
|
||||
new(instanceId, "deploy-" + instanceId, instanceId, DeploymentStatus.Success,
|
||||
null, "operator", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, null);
|
||||
|
||||
/// <summary>
|
||||
/// Stubs <c>QueryDeploymentSummariesAsync</c> so it applies the same
|
||||
/// instance / status / scope / paging predicates the database would.
|
||||
///
|
||||
/// <para>
|
||||
/// The filtering moved OUT of the handler and INTO the query, so a stub that
|
||||
/// ignored the arguments would make these tests pass no matter what the handler
|
||||
/// passed down. Honouring them here is what keeps the scope-enforcement
|
||||
/// assertions meaningful.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void StubDeploymentSummaryQuery(
|
||||
IDeploymentManagerRepository repo,
|
||||
IEnumerable<int> instanceIds)
|
||||
{
|
||||
var rows = instanceIds.Select(DeploymentSummaryFor).ToList();
|
||||
repo.QueryDeploymentSummariesAsync(
|
||||
Arg.Any<int?>(), Arg.Any<DeploymentStatus?>(), Arg.Any<IReadOnlyCollection<int>?>(),
|
||||
Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
var instanceId = ci.ArgAt<int?>(0);
|
||||
var scope = ci.ArgAt<IReadOnlyCollection<int>?>(2);
|
||||
var skip = ci.ArgAt<int>(3);
|
||||
var take = ci.ArgAt<int?>(4);
|
||||
|
||||
IEnumerable<DeploymentRecordSummary> q = rows;
|
||||
if (instanceId.HasValue)
|
||||
q = q.Where(r => r.InstanceId == instanceId.Value);
|
||||
if (scope != null)
|
||||
q = q.Where(r => scope.Contains(r.InstanceId));
|
||||
q = q.Skip(Math.Max(0, skip));
|
||||
if (take is > 0)
|
||||
q = q.Take(take.Value);
|
||||
return (IReadOnlyList<DeploymentRecordSummary>)q.ToList();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stubs <c>GetTemplateSummariesAsync</c> over <paramref name="count"/> synthetic
|
||||
/// templates, applying the requested Skip/Take exactly as the database would.
|
||||
/// </summary>
|
||||
private void StubTemplateSummaryPaging(int count)
|
||||
{
|
||||
var rows = Enumerable.Range(1, count)
|
||||
.Select(i => new TemplateSummary(i, $"T-{i:D3}", null, null, null, false, null, 0, 0, 0, 0, 0))
|
||||
.ToList();
|
||||
|
||||
_templateRepo.GetTemplateSummariesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
var skip = ci.ArgAt<int>(0);
|
||||
var take = ci.ArgAt<int?>(1);
|
||||
IEnumerable<TemplateSummary> q = rows.Skip(Math.Max(0, skip));
|
||||
if (take is > 0)
|
||||
q = q.Take(take.Value);
|
||||
return (IReadOnlyList<TemplateSummary>)q.ToList();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryDeployments_WithDesignRole_ReturnsUnauthorized()
|
||||
{
|
||||
@@ -1154,11 +1226,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
public void QueryDeployments_UnfilteredWithDeploymentRole_ReturnsAllRecords()
|
||||
{
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetAllDeploymentRecordsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord>
|
||||
{
|
||||
DeploymentRecordFor(1), DeploymentRecordFor(2)
|
||||
});
|
||||
StubDeploymentSummaryQuery(deployRepo, [1, 2]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1175,8 +1243,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
public void QueryDeployments_FilteredByInstanceId_ReturnsInstanceRecords()
|
||||
{
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetDeploymentsByInstanceIdAsync(5, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord> { DeploymentRecordFor(5) });
|
||||
StubDeploymentSummaryQuery(deployRepo, [5]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1205,7 +1272,8 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
// The out-of-scope instance's deployment history must not be queried.
|
||||
deployRepo.DidNotReceiveWithAnyArgs().GetDeploymentsByInstanceIdAsync(default);
|
||||
deployRepo.DidNotReceiveWithAnyArgs().QueryDeploymentSummariesAsync(
|
||||
default, default, default, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1214,8 +1282,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
_templateRepo.GetInstanceByIdAsync(5, Arg.Any<CancellationToken>())
|
||||
.Returns(new Instance("Pump5") { Id = 5, SiteId = 1 });
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetDeploymentsByInstanceIdAsync(5, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord> { DeploymentRecordFor(5) });
|
||||
StubDeploymentSummaryQuery(deployRepo, [5]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1240,11 +1307,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
new("Pump2") { Id = 2, SiteId = 2 },
|
||||
});
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetAllDeploymentRecordsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord>
|
||||
{
|
||||
DeploymentRecordFor(1), DeploymentRecordFor(2)
|
||||
});
|
||||
StubDeploymentSummaryQuery(deployRepo, [1, 2]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1276,12 +1339,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
new("Pump3") { Id = 3, SiteId = 1 },
|
||||
});
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetAllDeploymentRecordsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord>
|
||||
{
|
||||
DeploymentRecordFor(1), DeploymentRecordFor(2), DeploymentRecordFor(3),
|
||||
DeploymentRecordFor(1), DeploymentRecordFor(3) // duplicates: still no extra lookups
|
||||
});
|
||||
StubDeploymentSummaryQuery(deployRepo, [1, 2, 3]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1300,11 +1358,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
// Admin role bypasses site scoping even with PermittedSiteIds set.
|
||||
// (The user also holds Deployment so it passes the role gate.)
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
deployRepo.GetAllDeploymentRecordsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Commons.Entities.Deployment.DeploymentRecord>
|
||||
{
|
||||
DeploymentRecordFor(1), DeploymentRecordFor(2)
|
||||
});
|
||||
StubDeploymentSummaryQuery(deployRepo, [1, 2]);
|
||||
_services.AddScoped(_ => deployRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
@@ -1391,10 +1445,11 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
[Fact]
|
||||
public void ListTemplates_SkipTake_ReturnsRequestedWindow()
|
||||
{
|
||||
var templates = Enumerable.Range(1, 30)
|
||||
.Select(i => new Template($"T-{i:D3}") { Id = i })
|
||||
.ToList();
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>()).Returns(templates);
|
||||
// Paging moved into the repository (DB-side Skip/Take). The stub applies
|
||||
// the same window the database would, so this still pins that the
|
||||
// command's Skip/Take actually reach the query — the bug being that the
|
||||
// handler used to load everything and slice in memory.
|
||||
StubTemplateSummaryPaging(30);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new ListTemplatesCommand(Skip: 10, Take: 5)));
|
||||
@@ -1410,10 +1465,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
[Fact]
|
||||
public void ListTemplates_DefaultTake_ReturnsAll()
|
||||
{
|
||||
var templates = Enumerable.Range(1, 30)
|
||||
.Select(i => new Template($"T-{i:D3}") { Id = i })
|
||||
.ToList();
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>()).Returns(templates);
|
||||
StubTemplateSummaryPaging(30);
|
||||
|
||||
var actor = CreateActor();
|
||||
actor.Tell(Envelope(new ListTemplatesCommand())); // Take = null => unlimited
|
||||
@@ -1527,7 +1579,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
// Repository throws an unanticipated fault carrying sensitive-looking
|
||||
// detail. The raw text must NOT reach the caller.
|
||||
const string secret = "Server=db-internal-prod;constraint FK_secret";
|
||||
_templateRepo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
||||
_templateRepo.GetTemplateSummariesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidProgramException(secret));
|
||||
|
||||
var actor = CreateActor();
|
||||
|
||||
Reference in New Issue
Block a user