perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
+224
-2
@@ -3,20 +3,32 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||
|
||||
public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly ITemplateGraphWatermark _watermark;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TemplateEngineRepository class.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context used to access template and instance data.</param>
|
||||
public TemplateEngineRepository(ScadaBridgeDbContext context)
|
||||
/// <param name="watermark">
|
||||
/// Process-wide template/instance version watermark. This repository is the
|
||||
/// single unit-of-work through which EVERY template-graph mutation commits —
|
||||
/// <c>TemplateService</c>, the <c>ManagementActor</c> native-alarm-source
|
||||
/// handlers, and the Transport bundle importer all funnel through the same
|
||||
/// <see cref="SaveChangesAsync"/> — so bumping here (rather than in each
|
||||
/// caller) is the only placement that cannot be bypassed.
|
||||
/// </param>
|
||||
public TemplateEngineRepository(ScadaBridgeDbContext context, ITemplateGraphWatermark watermark)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_watermark = watermark ?? throw new ArgumentNullException(nameof(watermark));
|
||||
}
|
||||
|
||||
// Template
|
||||
@@ -78,6 +90,95 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Two deliberate departures from <see cref="GetAllTemplatesAsync"/>:
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <c>AsNoTracking</c> — the analysis walks (cycle detection, collision
|
||||
/// detection, canonical-name resolution) never write through the loaded
|
||||
/// graph, so paying for a change-tracker snapshot of every attribute,
|
||||
/// alarm, script and composition in the database is pure waste.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <c>TemplateScript.Code</c> is projected away (left empty). Script
|
||||
/// bodies are by far the largest column in the graph and NONE of the
|
||||
/// analysis consumers read them — <c>CycleDetector</c> reads only ids and
|
||||
/// edges, <c>CollisionDetector</c> and <c>TemplateResolver</c> read only
|
||||
/// member names and lock flags.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// Callers that DO need bodies or tracked entities — the inheritance
|
||||
/// reconciler and the flattener — must keep using
|
||||
/// <see cref="GetAllTemplatesAsync"/> / <see cref="GetTemplateWithChildrenAsync"/>.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<Template>> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
.AsNoTracking()
|
||||
.Select(t => new Template(t.Name)
|
||||
{
|
||||
Id = t.Id,
|
||||
Description = t.Description,
|
||||
ParentTemplateId = t.ParentTemplateId,
|
||||
FolderId = t.FolderId,
|
||||
IsDerived = t.IsDerived,
|
||||
OwnerCompositionId = t.OwnerCompositionId,
|
||||
Attributes = t.Attributes.ToList(),
|
||||
Alarms = t.Alarms.ToList(),
|
||||
NativeAlarmSources = t.NativeAlarmSources.ToList(),
|
||||
Compositions = t.Compositions.ToList(),
|
||||
Scripts = t.Scripts
|
||||
.Select(s => new TemplateScript(s.Name, string.Empty)
|
||||
{
|
||||
Id = s.Id,
|
||||
TemplateId = s.TemplateId,
|
||||
IsLocked = s.IsLocked,
|
||||
TriggerType = s.TriggerType,
|
||||
TriggerConfiguration = s.TriggerConfiguration,
|
||||
ParameterDefinitions = s.ParameterDefinitions,
|
||||
ReturnDefinition = s.ReturnDefinition,
|
||||
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
|
||||
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
|
||||
IsInherited = s.IsInherited,
|
||||
LockedInDerived = s.LockedInDerived
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateSummary>> GetTemplateSummariesAsync(
|
||||
int skip,
|
||||
int? take,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Templates
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Id)
|
||||
.Skip(Math.Max(0, skip));
|
||||
|
||||
if (take is > 0)
|
||||
query = query.Take(Math.Min(take.Value, TemplateSummary.MaxPageSize));
|
||||
|
||||
return await query
|
||||
.Select(t => new TemplateSummary(
|
||||
t.Id,
|
||||
t.Name,
|
||||
t.Description,
|
||||
t.ParentTemplateId,
|
||||
t.FolderId,
|
||||
t.IsDerived,
|
||||
t.OwnerCompositionId,
|
||||
t.Attributes.Count,
|
||||
t.Alarms.Count,
|
||||
t.Scripts.Count,
|
||||
t.Compositions.Count,
|
||||
t.NativeAlarmSources.Count))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -659,8 +760,129 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Inspects the change tracker BEFORE committing (afterwards every entry is
|
||||
/// <see cref="EntityState.Unchanged"/> and the attribution is lost) and bumps
|
||||
/// the <see cref="ITemplateGraphWatermark"/> for each affected template /
|
||||
/// instance. The bump is applied only AFTER a successful commit, so a failed
|
||||
/// save does not invalidate caches for a change that never landed.
|
||||
/// </remarks>
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
var pending = CollectWatermarkBumps();
|
||||
var written = await _context.SaveChangesAsync(cancellationToken);
|
||||
ApplyWatermarkBumps(pending);
|
||||
return written;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the watermark bumps implied by the current change-tracker
|
||||
/// contents. Captured pre-commit, applied post-commit.
|
||||
/// </summary>
|
||||
private readonly record struct WatermarkBumps(
|
||||
HashSet<int> Templates,
|
||||
HashSet<int> StructuralTemplates,
|
||||
HashSet<int> Instances,
|
||||
bool Unattributed);
|
||||
|
||||
/// <summary>
|
||||
/// Walks the change tracker and maps every added/modified/deleted
|
||||
/// template-graph or instance-graph entry back to the template (or instance)
|
||||
/// whose flattened output it can affect.
|
||||
///
|
||||
/// <para>
|
||||
/// A change is treated as STRUCTURAL — invalidating cached chain membership,
|
||||
/// not just chain contents — when it adds or removes a <see cref="Template"/>
|
||||
/// row, changes a <see cref="Template.ParentTemplateId"/>, or touches a
|
||||
/// <see cref="TemplateComposition"/> row. Those are exactly the edges the
|
||||
/// flattener walks to build a chain.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// An entry whose owning id cannot be resolved (a child row deleted before
|
||||
/// its FK was materialised, or an entity type added later that this switch
|
||||
/// does not know) sets <c>Unattributed</c>, which conservatively invalidates
|
||||
/// every cached chain rather than silently letting a stale entry survive.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private WatermarkBumps CollectWatermarkBumps()
|
||||
{
|
||||
var templates = new HashSet<int>();
|
||||
var structural = new HashSet<int>();
|
||||
var instances = new HashSet<int>();
|
||||
var unattributed = false;
|
||||
|
||||
foreach (var entry in _context.ChangeTracker.Entries())
|
||||
{
|
||||
if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted))
|
||||
continue;
|
||||
|
||||
switch (entry.Entity)
|
||||
{
|
||||
case Template t:
|
||||
templates.Add(t.Id);
|
||||
// Add/Delete changes chain membership; a re-parent changes it too.
|
||||
if (entry.State != EntityState.Modified
|
||||
|| entry.Property(nameof(Template.ParentTemplateId)).IsModified)
|
||||
{
|
||||
structural.Add(t.Id);
|
||||
}
|
||||
break;
|
||||
case TemplateAttribute a:
|
||||
templates.Add(a.TemplateId);
|
||||
break;
|
||||
case TemplateAlarm al:
|
||||
templates.Add(al.TemplateId);
|
||||
break;
|
||||
case TemplateScript s:
|
||||
templates.Add(s.TemplateId);
|
||||
break;
|
||||
case TemplateNativeAlarmSource ns:
|
||||
templates.Add(ns.TemplateId);
|
||||
break;
|
||||
case TemplateComposition c:
|
||||
templates.Add(c.TemplateId);
|
||||
structural.Add(c.TemplateId);
|
||||
break;
|
||||
case Instance i:
|
||||
instances.Add(i.Id);
|
||||
break;
|
||||
case InstanceAttributeOverride ao:
|
||||
instances.Add(ao.InstanceId);
|
||||
break;
|
||||
case InstanceAlarmOverride alo:
|
||||
instances.Add(alo.InstanceId);
|
||||
break;
|
||||
case InstanceNativeAlarmSourceOverride nso:
|
||||
instances.Add(nso.InstanceId);
|
||||
break;
|
||||
case InstanceConnectionBinding cb:
|
||||
instances.Add(cb.InstanceId);
|
||||
break;
|
||||
case SharedScript:
|
||||
// Shared scripts are a session-global input to validation, not
|
||||
// owned by any one template — invalidate everything.
|
||||
unattributed = true;
|
||||
break;
|
||||
default:
|
||||
// Areas, folders and anything else do not feed the flattener.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new WatermarkBumps(templates, structural, instances, unattributed);
|
||||
}
|
||||
|
||||
/// <summary>Applies a previously collected <see cref="WatermarkBumps"/> snapshot.</summary>
|
||||
private void ApplyWatermarkBumps(WatermarkBumps bumps)
|
||||
{
|
||||
if (bumps.Unattributed)
|
||||
_watermark.BumpAll();
|
||||
|
||||
foreach (var templateId in bumps.Templates)
|
||||
_watermark.BumpTemplate(templateId, bumps.StructuralTemplates.Contains(templateId));
|
||||
|
||||
foreach (var instanceId in bumps.Instances)
|
||||
_watermark.BumpInstance(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user