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
@@ -466,4 +466,17 @@ public interface IDeploymentManagerRepository
/// </summary>
/// <returns>The number of tracked entries detached (0 when nothing was pending).</returns>
int DetachPendingChanges();
/// <summary>
/// Stops tracking the SETTLED deployment rows of one instance — its
/// <see cref="PendingDeployment"/>, <see cref="DeployedConfigSnapshot"/> and
/// <see cref="DeploymentRecord"/> entries in the <c>Unchanged</c> state. A bulk site deploy
/// shares one scoped unit of work across every instance; without this, each finalised
/// instance's config JSON (staged copy, old and new snapshot) stays tracked for the rest of
/// the batch and every later <c>SaveChanges</c> re-scans it. Entries with pending writes are
/// left alone.
/// </summary>
/// <param name="instanceId">The instance whose settled rows to stop tracking.</param>
/// <returns>The number of entries detached.</returns>
int DetachSettledDeploymentRows(int instanceId);
}
@@ -844,6 +844,27 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
return pending.Count;
}
/// <inheritdoc />
public int DetachSettledDeploymentRows(int instanceId)
{
// Enumerated into a list first: changing State mutates the tracker.
var settled = _dbContext.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Unchanged
&& e.Entity switch
{
PendingDeployment p => p.InstanceId == instanceId,
DeployedConfigSnapshot s => s.InstanceId == instanceId,
DeploymentRecord r => r.InstanceId == instanceId,
_ => false
})
.ToList();
foreach (var entry in settled)
entry.State = EntityState.Detached;
return settled.Count;
}
/// <summary>
/// Flat shape of the deployment-record ⋈ instance join used to compose the list
/// query. A named type rather than an anonymous one so
@@ -1051,6 +1051,13 @@ public class DeploymentService
{
step.LockHandle?.Dispose();
}
// This instance is done. The batch shares ONE scoped unit of work, so
// without this its staged copy and old/new snapshot JSON would stay
// tracked (and re-scanned by every later SaveChanges) until the whole
// batch ends; dropping the step's own copy of the config lets it go too.
_repository.DetachSettledDeploymentRows(step.Instance.Id);
prepared[finalizedUpTo] = step with { ConfigJson = string.Empty };
}
}
catch (Exception ex)