Phase 3C: Deployment pipeline & Store-and-Forward engine
Deployment Manager (WP-1–8, WP-16): - DeploymentService: full pipeline (flatten→validate→send→track→audit) - OperationLockManager: per-instance concurrency control - StateTransitionValidator: Enabled/Disabled/NotDeployed transition matrix - ArtifactDeploymentService: broadcast to all sites with per-site results - Deployment identity (GUID + revision hash), idempotency, staleness detection - Instance lifecycle commands (disable/enable/delete) with deduplication Store-and-Forward (WP-9–15): - StoreAndForwardStorage: SQLite persistence, 3 categories, no max buffer - StoreAndForwardService: fixed-interval retry, transient-only buffering, parking - ReplicationService: async best-effort to standby (fire-and-forget) - Parked message management (query/retry/discard from central) - Messages survive instance deletion, S&F drains on disable 620 tests pass (+79 new), zero warnings.
This commit is contained in:
121
src/ScadaLink.DeploymentManager/FlatteningPipeline.cs
Normal file
121
src/ScadaLink.DeploymentManager/FlatteningPipeline.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using ScadaLink.Commons.Entities.Sites;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.Commons.Types.Flattening;
|
||||
using ScadaLink.TemplateEngine.Flattening;
|
||||
using ScadaLink.TemplateEngine.Validation;
|
||||
|
||||
namespace ScadaLink.DeploymentManager;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates the TemplateEngine services (FlatteningService, ValidationService, RevisionHashService)
|
||||
/// into a single pipeline for deployment use.
|
||||
///
|
||||
/// WP-16: This captures template state at the time of flatten, ensuring that concurrent template edits
|
||||
/// (last-write-wins) do not conflict with in-progress deployments.
|
||||
/// </summary>
|
||||
public class FlatteningPipeline : IFlatteningPipeline
|
||||
{
|
||||
private readonly ITemplateEngineRepository _templateRepo;
|
||||
private readonly ISiteRepository _siteRepo;
|
||||
private readonly FlatteningService _flatteningService;
|
||||
private readonly ValidationService _validationService;
|
||||
private readonly RevisionHashService _revisionHashService;
|
||||
|
||||
public FlatteningPipeline(
|
||||
ITemplateEngineRepository templateRepo,
|
||||
ISiteRepository siteRepo,
|
||||
FlatteningService flatteningService,
|
||||
ValidationService validationService,
|
||||
RevisionHashService revisionHashService)
|
||||
{
|
||||
_templateRepo = templateRepo;
|
||||
_siteRepo = siteRepo;
|
||||
_flatteningService = flatteningService;
|
||||
_validationService = validationService;
|
||||
_revisionHashService = revisionHashService;
|
||||
}
|
||||
|
||||
public async Task<Result<FlatteningPipelineResult>> FlattenAndValidateAsync(
|
||||
int instanceId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Load instance with full graph
|
||||
var instance = await _templateRepo.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
if (instance == null)
|
||||
return Result<FlatteningPipelineResult>.Failure($"Instance with ID {instanceId} not found.");
|
||||
|
||||
// Build template chain
|
||||
var templateChain = await BuildTemplateChainAsync(instance.TemplateId, cancellationToken);
|
||||
if (templateChain.Count == 0)
|
||||
return Result<FlatteningPipelineResult>.Failure("Template chain is empty.");
|
||||
|
||||
// Build composition maps
|
||||
var compositionMap = new Dictionary<int, IReadOnlyList<Commons.Entities.Templates.TemplateComposition>>();
|
||||
var composedChains = new Dictionary<int, IReadOnlyList<Commons.Entities.Templates.Template>>();
|
||||
|
||||
foreach (var template in templateChain)
|
||||
{
|
||||
var compositions = await _templateRepo.GetCompositionsByTemplateIdAsync(template.Id, cancellationToken);
|
||||
if (compositions.Count > 0)
|
||||
{
|
||||
compositionMap[template.Id] = compositions;
|
||||
foreach (var comp in compositions)
|
||||
{
|
||||
if (!composedChains.ContainsKey(comp.ComposedTemplateId))
|
||||
{
|
||||
composedChains[comp.ComposedTemplateId] =
|
||||
await BuildTemplateChainAsync(comp.ComposedTemplateId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load data connections for the site
|
||||
var dataConnections = await LoadDataConnectionsAsync(instance.SiteId, cancellationToken);
|
||||
|
||||
// Flatten
|
||||
var flattenResult = _flatteningService.Flatten(
|
||||
instance, templateChain, compositionMap, composedChains, dataConnections);
|
||||
|
||||
if (flattenResult.IsFailure)
|
||||
return Result<FlatteningPipelineResult>.Failure(flattenResult.Error);
|
||||
|
||||
var config = flattenResult.Value;
|
||||
|
||||
// Validate
|
||||
var validation = _validationService.Validate(config);
|
||||
|
||||
// Compute revision hash
|
||||
var hash = _revisionHashService.ComputeHash(config);
|
||||
|
||||
return Result<FlatteningPipelineResult>.Success(
|
||||
new FlatteningPipelineResult(config, hash, validation));
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<Commons.Entities.Templates.Template>> BuildTemplateChainAsync(
|
||||
int templateId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var chain = new List<Commons.Entities.Templates.Template>();
|
||||
var currentId = (int?)templateId;
|
||||
|
||||
while (currentId.HasValue)
|
||||
{
|
||||
var template = await _templateRepo.GetTemplateWithChildrenAsync(currentId.Value, cancellationToken);
|
||||
if (template == null) break;
|
||||
chain.Add(template);
|
||||
currentId = template.ParentTemplateId;
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<int, DataConnection>> LoadDataConnectionsAsync(
|
||||
int siteId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var connections = await _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, cancellationToken);
|
||||
return connections.ToDictionary(c => c.Id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user