perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
||||
@@ -23,6 +24,7 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
private readonly ValidationService _validationService;
|
||||
private readonly RevisionHashService _revisionHashService;
|
||||
private readonly ISharedSchemaRepository _sharedSchemaRepo;
|
||||
private readonly ITemplateGraphWatermark _watermark;
|
||||
|
||||
/// <summary>Initializes a new <see cref="FlatteningPipeline"/> with the required template engine and site repositories and services.</summary>
|
||||
/// <param name="templateRepo">Repository for loading templates and instance data.</param>
|
||||
@@ -35,13 +37,19 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
/// look up <c>lib:Name</c> library references so a dangling reference in any validated
|
||||
/// script schema becomes a deploy-blocking error.
|
||||
/// </param>
|
||||
/// <param name="watermark">
|
||||
/// Process-wide template version watermark. Supplies the per-template version
|
||||
/// used as the <see cref="FlattenSession"/> cache-key discriminator, so a
|
||||
/// memoised chain is dropped the moment any of its templates is edited.
|
||||
/// </param>
|
||||
public FlatteningPipeline(
|
||||
ITemplateEngineRepository templateRepo,
|
||||
ISiteRepository siteRepo,
|
||||
FlatteningService flatteningService,
|
||||
ValidationService validationService,
|
||||
RevisionHashService revisionHashService,
|
||||
ISharedSchemaRepository sharedSchemaRepo)
|
||||
ISharedSchemaRepository sharedSchemaRepo,
|
||||
ITemplateGraphWatermark watermark)
|
||||
{
|
||||
_templateRepo = templateRepo;
|
||||
_siteRepo = siteRepo;
|
||||
@@ -49,21 +57,35 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
_validationService = validationService;
|
||||
_revisionHashService = revisionHashService;
|
||||
_sharedSchemaRepo = sharedSchemaRepo;
|
||||
_watermark = watermark;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FlattenSession CreateSession() => new(_watermark);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<FlatteningPipelineResult>> FlattenAndValidateAsync(
|
||||
int instanceId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool validateScripts = true)
|
||||
bool validateScripts = true,
|
||||
FlattenSession? session = null)
|
||||
{
|
||||
// A caller that supplies no session gets a private one. That still pays
|
||||
// off inside a single flatten: the composed-chain walk below reaches the
|
||||
// same template through several composing parents, and the memo collapses
|
||||
// those to one load each.
|
||||
session ??= CreateSession();
|
||||
|
||||
// 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.");
|
||||
|
||||
Task<Commons.Entities.Templates.Template?> LoadTemplate(int id) =>
|
||||
_templateRepo.GetTemplateWithChildrenAsync(id, cancellationToken);
|
||||
|
||||
// Build template chain
|
||||
var templateChain = await BuildTemplateChainAsync(instance.TemplateId, cancellationToken);
|
||||
var templateChain = await session.GetChainAsync(instance.TemplateId, LoadTemplate);
|
||||
if (templateChain.Count == 0)
|
||||
return Result<FlatteningPipelineResult>.Failure("Template chain is empty.");
|
||||
|
||||
@@ -84,7 +106,9 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
{
|
||||
if (!processedTemplateIds.Add(template.Id)) continue;
|
||||
|
||||
var compositions = await _templateRepo.GetCompositionsByTemplateIdAsync(template.Id, cancellationToken);
|
||||
var compositions = await session.GetCompositionsAsync(
|
||||
template.Id,
|
||||
id => _templateRepo.GetCompositionsByTemplateIdAsync(id, cancellationToken));
|
||||
if (compositions.Count == 0) continue;
|
||||
|
||||
compositionMap[template.Id] = compositions;
|
||||
@@ -92,7 +116,7 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
{
|
||||
if (composedChains.ContainsKey(comp.ComposedTemplateId)) continue;
|
||||
|
||||
var composedChain = await BuildTemplateChainAsync(comp.ComposedTemplateId, cancellationToken);
|
||||
var composedChain = await session.GetChainAsync(comp.ComposedTemplateId, LoadTemplate);
|
||||
composedChains[comp.ComposedTemplateId] = composedChain;
|
||||
pendingChains.Enqueue(composedChain);
|
||||
}
|
||||
@@ -100,7 +124,9 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
}
|
||||
|
||||
// Load data connections for the site
|
||||
var dataConnections = await LoadDataConnectionsAsync(instance.SiteId, cancellationToken);
|
||||
var dataConnections = await session.GetDataConnectionsAsync(
|
||||
instance.SiteId,
|
||||
siteId => LoadDataConnectionsAsync(siteId, cancellationToken));
|
||||
|
||||
// Flatten
|
||||
var flattenResult = _flatteningService.Flatten(
|
||||
@@ -111,15 +137,20 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
|
||||
var config = flattenResult.Value;
|
||||
|
||||
// Load shared scripts for semantic validation
|
||||
var sharedScriptEntities = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken);
|
||||
var resolvedSharedScripts = sharedScriptEntities.Select(s => new ResolvedScript
|
||||
// Load shared scripts for semantic validation. Session-global: identical
|
||||
// for every instance in a batch, so hoisted to one query per session
|
||||
// (the ArtifactDeploymentService global-hoist shape).
|
||||
var resolvedSharedScripts = await session.GetSharedScriptsAsync(async () =>
|
||||
{
|
||||
CanonicalName = s.Name,
|
||||
Code = s.Code,
|
||||
ParameterDefinitions = s.ParameterDefinitions,
|
||||
ReturnDefinition = s.ReturnDefinition
|
||||
}).ToList();
|
||||
var sharedScriptEntities = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken);
|
||||
return (IReadOnlyList<ResolvedScript>)sharedScriptEntities.Select(s => new ResolvedScript
|
||||
{
|
||||
CanonicalName = s.Name,
|
||||
Code = s.Code,
|
||||
ParameterDefinitions = s.ParameterDefinitions,
|
||||
ReturnDefinition = s.ReturnDefinition
|
||||
}).ToList();
|
||||
});
|
||||
|
||||
// Compute the alarm-capable connection-name set so the semantic validator
|
||||
// can gate native-alarm-source bindings. "Alarm-capable" matches the DCL
|
||||
@@ -149,8 +180,13 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
// pre-loaded once into a name→JSON map here (avoiding sync-over-async) and the
|
||||
// seam is a pure in-memory lookup. An unresolved {"$ref":"lib:Name"} in any
|
||||
// validated script schema then becomes a deploy-blocking SchemaReference error.
|
||||
var sharedSchemas = await _sharedSchemaRepo.ListAsync(cancellationToken);
|
||||
var schemaLibrary = sharedSchemas.ToDictionary(s => s.Name, s => s.SchemaJson, StringComparer.Ordinal);
|
||||
// Session-global, same as the shared scripts above.
|
||||
var schemaLibrary = await session.GetSchemaLibraryAsync(async () =>
|
||||
{
|
||||
var sharedSchemas = await _sharedSchemaRepo.ListAsync(cancellationToken);
|
||||
return (IReadOnlyDictionary<string, string>)sharedSchemas
|
||||
.ToDictionary(s => s.Name, s => s.SchemaJson, StringComparer.Ordinal);
|
||||
});
|
||||
Func<string, string?> resolveSchemaRef = name => schemaLibrary.GetValueOrDefault(name);
|
||||
|
||||
// Validate. This is the deploy-gating path, so connection-binding completeness
|
||||
@@ -180,24 +216,6 @@ public class FlatteningPipeline : IFlatteningPipeline
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user