perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
+83
@@ -277,6 +277,89 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
return expired.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeploymentRecordSummary>> QueryDeploymentSummariesAsync(
|
||||
int? instanceId,
|
||||
DeploymentStatus? status,
|
||||
IReadOnlyCollection<int>? instanceIdScope,
|
||||
int skip,
|
||||
int? take,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _dbContext.DeploymentRecords.AsNoTracking();
|
||||
|
||||
if (instanceId.HasValue)
|
||||
query = query.Where(d => d.InstanceId == instanceId.Value);
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(d => d.Status == status.Value);
|
||||
|
||||
if (instanceIdScope != null)
|
||||
{
|
||||
// Site scoping applied DB-side. An empty scope is a real, meaningful
|
||||
// filter — a user permitted no in-scope instances sees nothing — so it
|
||||
// must not be short-circuited into "no filter".
|
||||
var scope = instanceIdScope as int[] ?? instanceIdScope.ToArray();
|
||||
query = query.Where(d => scope.Contains(d.InstanceId));
|
||||
}
|
||||
|
||||
// ThenByDescending(Id) for the same reason GetCurrentDeploymentStatusAsync
|
||||
// does it: DeployedAt ties on rapid redeploys, and an unstable sort key
|
||||
// makes Skip/Take paging non-deterministic (rows repeat or vanish between
|
||||
// pages).
|
||||
var paged = query
|
||||
.OrderByDescending(d => d.DeployedAt)
|
||||
.ThenByDescending(d => d.Id)
|
||||
.Skip(Math.Max(0, skip));
|
||||
|
||||
if (take is > 0)
|
||||
paged = paged.Take(Math.Min(take.Value, DeploymentRecordSummary.MaxPageSize));
|
||||
|
||||
return await paged
|
||||
.Select(d => new DeploymentRecordSummary(
|
||||
d.Id,
|
||||
d.DeploymentId,
|
||||
d.InstanceId,
|
||||
d.Status,
|
||||
d.RevisionHash,
|
||||
d.DeployedBy,
|
||||
d.DeployedAt,
|
||||
d.CompletedAt,
|
||||
d.ErrorMessage))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> PurgeTerminalDeploymentRecordsAsync(
|
||||
DateTimeOffset cutoffUtc,
|
||||
int batchSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Terminal statuses ONLY. An InProgress (or Pending) row is never purged
|
||||
// regardless of age: it is precisely the row TryReconcileWithSiteAsync
|
||||
// looks for when deciding whether a prior deploy actually landed, and
|
||||
// deleting it would silently disable the query-before-redeploy idempotency
|
||||
// guard for that instance.
|
||||
//
|
||||
// Bounded batch rather than one unbounded DELETE so the first purge on a
|
||||
// long-lived system cannot take a table-scale lock on DeploymentRecords.
|
||||
// The caller re-invokes until it returns fewer rows than the batch size.
|
||||
var stale = await _dbContext.DeploymentRecords
|
||||
.Where(d => d.CompletedAt != null
|
||||
&& d.CompletedAt < cutoffUtc
|
||||
&& (d.Status == DeploymentStatus.Success || d.Status == DeploymentStatus.Failed))
|
||||
.OrderBy(d => d.Id)
|
||||
.Take(batchSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (stale.Count == 0)
|
||||
return 0;
|
||||
|
||||
_dbContext.DeploymentRecords.RemoveRange(stale);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
return stale.Count;
|
||||
}
|
||||
|
||||
// --- Startup reconciliation ---
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user