Add queue hardening by modifying DeploymentJobTargets, DeploymentJobSteps, and DeploymentJobs tables

- Drop existing indexes on DeploymentJobTargets and DeploymentJobSteps
- Alter Status column to nvarchar(450) in DeploymentJobTargets, DeploymentJobSteps, and DeploymentJobs
- Add new columns: Finished, OutputMetadataJson, Started to DeploymentJobTargets; ErrorMessage, Finished, OutputMetadataJson, Started to DeploymentJobSteps; CorrelationId, HeartbeatAt, Priority, RowVersion, ScheduledAt, WorkerName to DeploymentJobs
- Create new indexes for improved query performance
- Add constraints to ensure OutputMetadataJson is valid JSON
- Record migration in __EFMigrationsHistory
This commit is contained in:
Torsten Brendgen
2026-07-09 08:50:09 +02:00
parent 97238c28c9
commit 01c2a5df2e
45 changed files with 8198 additions and 75 deletions

View File

@@ -37,6 +37,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
{
Type = QueueJobType.TemplateJsonChanged,
Status = QueueJobStatus.Pending,
CorrelationId = Guid.NewGuid(),
PayloadJson = JsonSerializer.Serialize(payload),
Targets = deployments.Select(deployment => new QueueJobTargetModel
{
@@ -66,6 +67,10 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
.Include(group => group.Template)
.ThenInclude(template => template.DeploymentRule)
.ThenInclude(rule => rule.Steps)
.Include(group => group.TemplateSelections)
.ThenInclude(selection => selection.TemplateVersion)
.ThenInclude(version => version.Template)
.Include(group => group.TargetAssignments)
.Include(group => group.DeploymentRule)
.ThenInclude(rule => rule!.Steps)
.FirstOrDefault(group => group.Id == deploymentGroupId);
@@ -75,11 +80,23 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
throw new InvalidOperationException("DeploymentGroup does not exist.");
}
var templateId = deploymentGroup.TemplateId;
var primaryTemplateSelection = deploymentGroup.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.FirstOrDefault();
var templateId = primaryTemplateSelection?.TemplateVersion.TemplateId ?? deploymentGroup.TemplateId;
var resolvedTargetIds = targetIds
.Distinct()
.ToList();
if (resolvedTargetIds.Count == 0)
{
resolvedTargetIds = deploymentGroup.TargetAssignments
.OrderBy(assignment => assignment.SortOrder)
.Select(assignment => assignment.TargetId)
.Distinct()
.ToList();
}
if (resolvedTargetIds.Count == 0)
{
throw new InvalidOperationException("No target Targets provided.");
@@ -133,6 +150,31 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
DeploymentGroupId = deploymentGroupId,
TemplateId = templateId,
DeploymentRuleId = resolvedRule?.Id,
TemplateSelections = deploymentGroup.TemplateSelections
.OrderBy(selection => selection.SortOrder)
.Select(selection => new
{
selection.Id,
selection.TemplateVersionId,
TemplateId = selection.TemplateVersion.TemplateId,
TemplateName = selection.TemplateVersion.Template.Name,
selection.TemplateVersion.Version,
selection.TemplateVersion.JsonHash,
selection.TemplateRole,
selection.SortOrder,
selection.Alias
}),
TargetAssignments = deploymentGroup.TargetAssignments
.OrderBy(assignment => assignment.SortOrder)
.Where(assignment => resolvedTargetIds.Contains(assignment.TargetId))
.Select(assignment => new
{
assignment.Id,
assignment.TargetId,
assignment.RoleKey,
assignment.SortOrder,
assignment.NodeDataJson
}),
TargetIds = resolvedTargetIds,
JsonData = jsonData,
TargetCount = resolvedTargetIds.Count,
@@ -143,6 +185,7 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
{
Type = QueueJobType.DeploymentRequested,
Status = QueueJobStatus.Pending,
CorrelationId = Guid.NewGuid(),
PayloadJson = JsonSerializer.Serialize(payload),
RuleSnapshotJson = resolvedRule != null
? SerializeRuleSnapshot(resolvedRule)
@@ -179,6 +222,8 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
queueJob.Finished = null;
queueJob.LockedUntil = null;
queueJob.LockedBy = null;
queueJob.HeartbeatAt = null;
queueJob.WorkerName = null;
queueJob.Steps ??= new List<QueueJobStepModel>();
foreach (var target in queueJob.Targets)
@@ -187,6 +232,9 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
{
target.Status = QueueJobStatus.Pending;
target.ErrorMessage = null;
target.Started = null;
target.Finished = null;
target.OutputMetadataJson = null;
}
}
foreach (var step in queueJob.Steps)
@@ -200,6 +248,10 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
step.ApprovedAt = null;
step.ApprovedBy = null;
step.ApprovalComment = null;
step.Started = null;
step.Finished = null;
step.ErrorMessage = null;
step.OutputMetadataJson = null;
}
}
@@ -224,6 +276,8 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
step.QueueJob.Status = QueueJobStatus.Pending;
step.QueueJob.LockedUntil = null;
step.QueueJob.LockedBy = null;
step.QueueJob.HeartbeatAt = null;
step.QueueJob.WorkerName = null;
step.QueueJob.ErrorMessage = null;
return _context.SaveChanges() > 0;
@@ -248,6 +302,8 @@ namespace Microsoft.SelfService.Portal.Core.API.Services
step.QueueJob.Finished = DateTime.UtcNow;
step.QueueJob.LockedUntil = null;
step.QueueJob.LockedBy = null;
step.QueueJob.HeartbeatAt = null;
step.QueueJob.WorkerName = null;
step.QueueJob.ErrorMessage = comment ?? "Deployment step rejected.";
return _context.SaveChanges() > 0;