fix(deployment): detach each instance's settled rows from the bulk deploy's shared unit of work after Finalize (review 2026-10-21 04/M3)

This commit is contained in:
Joseph Doherty
2026-10-22 06:48:28 -04:00
parent 32e63eaa96
commit 7aacc49043
6 changed files with 100 additions and 1 deletions
@@ -3,6 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
@@ -416,4 +417,37 @@ public class PendingDeploymentRepositoryTests : IDisposable
var row = await _repository.GetPendingDeploymentByIdAsync("dep-x");
Assert.Equal("{\"fresh\":true}", row!.ConfigurationJson);
}
/// <summary>
/// review 2026-10-21 04/M3: detaching an instance's settled rows stops tracking its saved
/// pending row, snapshot and deployment record, and leaves another instance's rows and any
/// entry with a pending write alone.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DetachSettledDeploymentRows_DetachesOnlyThatInstancesUnchangedRows()
{
var mine = await SeedInstanceAsync("InstM3a");
var other = await SeedInstanceAsync("InstM3b");
await _repository.AddPendingDeploymentAsync(NewPending("m3-mine", mine, "{}"));
await _repository.AddPendingDeploymentAsync(NewPending("m3-other", other, "{}"));
await _repository.AddDeployedSnapshotAsync(new DeployedConfigSnapshot("m3-mine", "rev", "{}") { InstanceId = mine });
var record = new DeploymentRecord("m3-mine", "admin") { InstanceId = mine, Status = DeploymentStatus.Success };
await _repository.AddDeploymentRecordAsync(record);
await _repository.SaveChangesAsync();
var detached = _repository.DetachSettledDeploymentRows(mine);
Assert.Equal(3, detached);
Assert.DoesNotContain(_context.ChangeTracker.Entries<PendingDeployment>(), e => e.Entity.InstanceId == mine);
Assert.Empty(_context.ChangeTracker.Entries<DeployedConfigSnapshot>());
Assert.Empty(_context.ChangeTracker.Entries<DeploymentRecord>());
Assert.Contains(_context.ChangeTracker.Entries<PendingDeployment>(), e => e.Entity.InstanceId == other);
// A row with a pending write is never detached.
await _repository.AddDeploymentRecordAsync(
new DeploymentRecord("m3-unsaved", "admin") { InstanceId = mine, Status = DeploymentStatus.InProgress });
Assert.Equal(0, _repository.DetachSettledDeploymentRows(mine));
Assert.Single(_context.ChangeTracker.Entries<DeploymentRecord>());
}
}
@@ -592,6 +592,26 @@ public class DeploySiteAsyncTests : TestKit
Assert.All(sessions, s => Assert.Same(sessions[0], s));
}
/// <summary>
/// review 2026-10-21 04/M3: once an instance is finalised its settled deployment rows are
/// detached from the batch's shared unit of work, so N instances' config JSON is not held
/// (and re-scanned by every later save) for the rest of the batch.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeploySiteAsync_DetachesEachInstancesSettledRowsAfterFinalize()
{
ArrangeInstances(3);
var applied = new ConcurrentBag<string>();
var commActor = Sys.ActorOf(Props.Create(() => new RecordingSiteActor(applied)));
var result = await CreateService(commActor, maxParallelism: 3).DeploySiteAsync(SiteId, "admin");
Assert.Equal(3, result.Value.SuccessCount);
foreach (var id in new[] { 1, 2, 3 })
_repo.Received(1).DetachSettledDeploymentRows(id);
}
/// <summary>
/// <c>force</c> restores the unconditional behaviour for operators whose
/// site state central cannot see — a restored data volume or a rebuilt node