perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries

This commit is contained in:
Joseph Doherty
2026-08-14 21:14:22 -04:00
parent ee193cd2bb
commit 48b3c40a7f
59 changed files with 3051 additions and 231 deletions
@@ -22,6 +22,48 @@ public interface IDeploymentManagerRepository
/// <returns>A read-only list of all deployment records.</returns>
Task<IReadOnlyList<DeploymentRecord>> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Database-side paged and filtered deployment query returning row summaries.
/// Backs the <c>QueryDeployments</c> management command, which previously
/// loaded the ENTIRE deployment-record table (insert-only, one row per deploy
/// attempt for the life of the system) and ignored the page arguments the CLI
/// was already sending.
/// </summary>
/// <param name="instanceId">Restrict to one instance's history, or <see langword="null"/> for all.</param>
/// <param name="status">Restrict to one deployment status, or <see langword="null"/> for all.</param>
/// <param name="instanceIdScope">
/// When non-null, restrict to these instance ids. Used to apply a site-scoped
/// user's permitted-site filter IN THE DATABASE rather than by loading every
/// record and every instance and filtering in memory.
/// </param>
/// <param name="skip">Rows to skip; negative values are floored at 0.</param>
/// <param name="take">Page size; non-positive or null returns every remaining row.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>One page of deployment summaries, newest first.</returns>
Task<IReadOnlyList<Types.Deployment.DeploymentRecordSummary>> QueryDeploymentSummariesAsync(
int? instanceId,
DeploymentStatus? status,
IReadOnlyCollection<int>? instanceIdScope,
int skip,
int? take,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes TERMINAL deployment records (<see cref="DeploymentStatus.Success"/> /
/// <see cref="DeploymentStatus.Failed"/>) that completed before
/// <paramref name="cutoffUtc"/>, in one bounded batch.
///
/// <para>
/// Non-terminal rows are never touched: an <c>InProgress</c> record is what the
/// query-before-redeploy reconciliation path reads to decide whether a prior
/// deploy actually landed at the site, so purging one by age would silently
/// disable that idempotency guard.
/// </para>
/// </summary>
/// <param name="cutoffUtc">Records completing before this instant are eligible.</param>
/// <param name="batchSize">Maximum rows deleted in this call.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The number of rows deleted; fewer than <paramref name="batchSize"/> means the sweep is complete.</returns>
Task<int> PurgeTerminalDeploymentRecordsAsync(DateTimeOffset cutoffUtc, int batchSize, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all deployment records for a specific instance.
/// </summary>
/// <param name="instanceId">The instance ID.</param>
@@ -35,6 +35,41 @@ public interface ITemplateEngineRepository
/// <returns>A task that resolves to a read-only list of all templates.</returns>
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Read-only, no-tracking variant of <see cref="GetAllTemplatesAsync"/> for the
/// design-time ANALYSIS walks — acyclicity (<c>CycleDetector</c>), naming
/// collisions (<c>CollisionDetector</c>) and canonical-name resolution
/// (<c>TemplateResolver</c>). Script bodies are NOT loaded
/// (<see cref="TemplateScript.Code"/> comes back empty) because no analysis
/// consumer reads them; everything those walks do read — ids, parent and
/// composition edges, member names and lock flags — is present.
///
/// <para>
/// Do NOT use this for the inheritance reconciler or the flattener: the
/// reconciler compares and copies script bodies and writes through tracked
/// entities, and the flattener must observe rows an in-flight bundle import
/// has staged on the shared change tracker. Both keep using
/// <see cref="GetAllTemplatesAsync"/> / <see cref="GetTemplateWithChildrenAsync"/>.
/// </para>
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all templates without script bodies.</returns>
Task<IReadOnlyList<Template>> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Database-side paged listing of templates as row summaries (child
/// collections reduced to counts, no script bodies). Backs the
/// <c>ListTemplates</c> management command, which previously materialised
/// every template's full child graph and then paged the list in memory.
/// </summary>
/// <param name="skip">Number of rows to skip; negative values are floored at 0.</param>
/// <param name="take">
/// Page size. <see langword="null"/> or non-positive returns every remaining
/// row (the historical unpaged behaviour); otherwise clamped to
/// <see cref="Types.Templates.TemplateSummary.MaxPageSize"/>.
/// </param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to one page of template summaries, ordered by id.</returns>
Task<IReadOnlyList<Types.Templates.TemplateSummary>> GetTemplateSummariesAsync(int skip, int? take, CancellationToken cancellationToken = default);
/// <summary>
/// Returns every template that contains a composition referencing
/// <paramref name="composedTemplateId"/>. Each result is eager-loaded with
/// its Attributes / Scripts / Compositions so the caller can build a
@@ -0,0 +1,82 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
/// <summary>
/// Process-wide monotonic version watermark over the template/instance
/// configuration graph.
///
/// <para>
/// Every mutation that lands through the configuration database bumps the
/// version of the affected template (or instance), plus a global counter and —
/// for edge-shaped changes (template added/removed, parent or composition edge
/// changed) — a separate <see cref="StructureVersion"/>. Consumers use the
/// watermark two ways:
/// </para>
/// <list type="bullet">
/// <item>
/// as the discriminator in a flatten-session cache key
/// (<c>templateId + version</c>), so a memoised template chain can never be
/// served after that template changed; and
/// </item>
/// <item>
/// as a staleness fast path — when neither the instance nor any template in
/// its chain has moved since a previously computed revision hash, the hash
/// is still current and a full re-flatten can be skipped.
/// </item>
/// </list>
///
/// <para>
/// Versions are in-memory and process-local. They are deliberately NOT
/// persisted: a restart (or the standby central node) simply starts from zero,
/// which invalidates every cached entry and falls back to the authoritative
/// full flatten. The watermark is therefore only ever able to cause EXTRA work,
/// never stale work — a correctness property that matters because the value is
/// not replicated between the two central nodes.
/// </para>
/// </summary>
public interface ITemplateGraphWatermark
{
/// <summary>
/// Monotonic counter bumped on every template- or instance-graph mutation.
/// Useful as a coarse "did anything change at all" gate.
/// </summary>
long Global { get; }
/// <summary>
/// Monotonic counter bumped only when the SHAPE of the graph changes — a
/// template row added or removed, a parent-template edge changed, or a
/// composition row added/removed/repointed. A cached template-chain
/// MEMBERSHIP is valid only while this value is unchanged; the per-template
/// versions then cover the contents of each member.
/// </summary>
long StructureVersion { get; }
/// <summary>Current version of a single template. Zero when never mutated in this process.</summary>
/// <param name="templateId">Template id to read.</param>
/// <returns>The template's monotonic version.</returns>
long GetTemplateVersion(int templateId);
/// <summary>Current version of a single instance (its override/binding rows). Zero when never mutated in this process.</summary>
/// <param name="instanceId">Instance id to read.</param>
/// <returns>The instance's monotonic version.</returns>
long GetInstanceVersion(int instanceId);
/// <summary>Records a mutation of the given template.</summary>
/// <param name="templateId">Template whose version is bumped.</param>
/// <param name="structural">
/// When <c>true</c> the change altered the graph shape (add/remove/parent or
/// composition edge) and <see cref="StructureVersion"/> is bumped too.
/// </param>
void BumpTemplate(int templateId, bool structural = false);
/// <summary>Records a mutation of the given instance.</summary>
/// <param name="instanceId">Instance whose version is bumped.</param>
void BumpInstance(int instanceId);
/// <summary>
/// Records a mutation whose affected template/instance could not be
/// attributed to a specific id. Bumps <see cref="Global"/> and
/// <see cref="StructureVersion"/>, invalidating every chain-membership and
/// staleness fast path — the safe, conservative fallback.
/// </summary>
void BumpAll();
}