perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
+42
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
|
||||
public record MgmtDeployArtifactsCommand(int? SiteId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Deploys every deployable instance at one site in a single operation.
|
||||
///
|
||||
/// <para>
|
||||
/// Distinct from <see cref="MgmtDeployArtifactsCommand"/>, which ships the
|
||||
/// SYSTEM-WIDE artifact sets (shared scripts, external systems, DB/data
|
||||
/// connections) to a site. This command deploys the site's INSTANCE
|
||||
/// configurations — the same per-instance pipeline
|
||||
/// <c>MgmtDeployInstanceCommand</c> runs, batched.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Not all-or-nothing: each instance keeps its own deployment id, revision hash,
|
||||
/// operation lock and status record, and a failure on one is reported as a failed
|
||||
/// row while the rest proceed. Individually retryable via the single-instance
|
||||
/// deploy. Additive contract; new fields go on the end with a default.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="SiteId">Database id of the target site. Required — this command is never fleet-wide.</param>
|
||||
public record MgmtDeploySiteCommand(int SiteId);
|
||||
public record QueryDeploymentsCommand(int? InstanceId = null, string? Status = null, int Page = 1, int PageSize = 50);
|
||||
public record GetDeploymentDiffCommand(int InstanceId);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
||||
|
||||
/// <summary>
|
||||
/// Row-shaped projection of a <c>DeploymentRecord</c> for list surfaces (CLI
|
||||
/// <c>deploy list</c> / <c>instance</c> history, the Central UI deployment-status
|
||||
/// page).
|
||||
///
|
||||
/// <para>
|
||||
/// Exists so a deployment listing can be paged and filtered in the DATABASE.
|
||||
/// Deployment records are insert-only — one row per deploy attempt, retained for
|
||||
/// the configured retention window — so the previous "load the whole table, then
|
||||
/// filter in memory" read path degraded with the age of the system rather than
|
||||
/// with the size of the requested page.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The <c>RowVersion</c> optimistic-concurrency token is deliberately absent: this
|
||||
/// is a READ projection, and a summary must never be mistaken for something that
|
||||
/// can be written back. Mutating paths keep loading the tracked entity.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Message-contract evolution rule: additive-only. New fields go on the end with a
|
||||
/// default.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="Id">Deployment record row id.</param>
|
||||
/// <param name="DeploymentId">The logical deployment id (GUID, "N" format).</param>
|
||||
/// <param name="InstanceId">Instance the deployment targeted.</param>
|
||||
/// <param name="Status">Terminal or in-flight deployment status.</param>
|
||||
/// <param name="RevisionHash">Revision hash of the deployed configuration.</param>
|
||||
/// <param name="DeployedBy">User who initiated the deployment.</param>
|
||||
/// <param name="DeployedAt">When the deployment was initiated.</param>
|
||||
/// <param name="CompletedAt">When the deployment reached a terminal status, if it has.</param>
|
||||
/// <param name="ErrorMessage">Failure detail when the deployment did not succeed.</param>
|
||||
public record DeploymentRecordSummary(
|
||||
int Id,
|
||||
string DeploymentId,
|
||||
int InstanceId,
|
||||
DeploymentStatus Status,
|
||||
string? RevisionHash,
|
||||
string DeployedBy,
|
||||
DateTimeOffset DeployedAt,
|
||||
DateTimeOffset? CompletedAt,
|
||||
string? ErrorMessage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Hard ceiling applied to a requested page size, mirroring the in-memory
|
||||
/// <c>Page</c> clamp the management actor already applies elsewhere.
|
||||
/// </summary>
|
||||
public const int MaxPageSize = 1000;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory <see cref="ITemplateGraphWatermark"/>. Registered as a singleton so
|
||||
/// the writer (the configuration-database repository's unit-of-work commit) and
|
||||
/// the readers (the flatten-session cache, the staleness fast path) observe the
|
||||
/// same counters within a process.
|
||||
///
|
||||
/// <para>
|
||||
/// Lives in Commons rather than a component project because both the
|
||||
/// ConfigurationDatabase writer and the DeploymentManager readers need it and
|
||||
/// neither references the other. It is a pure in-memory primitive with no
|
||||
/// infrastructure dependency, so it does not violate the "implementations live
|
||||
/// with their component" convention the way a repository would.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// All counters are <see cref="Interlocked"/>-updated and the per-id maps are
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}"/>, so concurrent management
|
||||
/// commands (which each run on their own DI scope) can bump safely. Reads are
|
||||
/// deliberately unsynchronised relative to each other: a reader that observes a
|
||||
/// half-applied batch of bumps sees a CHANGED value for at least one member,
|
||||
/// which invalidates rather than falsely validates a cache entry.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class TemplateGraphWatermark : ITemplateGraphWatermark
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, long> _templateVersions = new();
|
||||
private readonly ConcurrentDictionary<int, long> _instanceVersions = new();
|
||||
private long _global;
|
||||
private long _structure;
|
||||
|
||||
/// <inheritdoc />
|
||||
public long Global => Interlocked.Read(ref _global);
|
||||
|
||||
/// <inheritdoc />
|
||||
public long StructureVersion => Interlocked.Read(ref _structure);
|
||||
|
||||
/// <inheritdoc />
|
||||
public long GetTemplateVersion(int templateId) =>
|
||||
_templateVersions.TryGetValue(templateId, out var v) ? v : 0L;
|
||||
|
||||
/// <inheritdoc />
|
||||
public long GetInstanceVersion(int instanceId) =>
|
||||
_instanceVersions.TryGetValue(instanceId, out var v) ? v : 0L;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BumpTemplate(int templateId, bool structural = false)
|
||||
{
|
||||
_templateVersions.AddOrUpdate(templateId, 1L, static (_, current) => current + 1);
|
||||
Interlocked.Increment(ref _global);
|
||||
if (structural)
|
||||
Interlocked.Increment(ref _structure);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BumpInstance(int instanceId)
|
||||
{
|
||||
_instanceVersions.AddOrUpdate(instanceId, 1L, static (_, current) => current + 1);
|
||||
Interlocked.Increment(ref _global);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BumpAll()
|
||||
{
|
||||
Interlocked.Increment(ref _global);
|
||||
Interlocked.Increment(ref _structure);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// Row-shaped projection of a template for list surfaces (CLI <c>template list</c>,
|
||||
/// the Central UI template tree, the <c>ListTemplates</c> management command).
|
||||
///
|
||||
/// <para>
|
||||
/// Exists so a template listing can be paged in the DATABASE rather than by
|
||||
/// materialising every template's full child graph — attributes, alarms, script
|
||||
/// bodies, compositions and native alarm sources — and then discarding all but
|
||||
/// one page of it in memory. Child collections are reduced to counts, which is
|
||||
/// what every list surface actually renders.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Message-contract evolution rule: this record is additive-only. New fields go
|
||||
/// on the end with a default so an older peer deserialising a newer payload is
|
||||
/// unaffected.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="Id">Template id.</param>
|
||||
/// <param name="Name">Template name.</param>
|
||||
/// <param name="Description">Optional description.</param>
|
||||
/// <param name="ParentTemplateId">Parent template id when this template inherits.</param>
|
||||
/// <param name="FolderId">Containing folder id.</param>
|
||||
/// <param name="IsDerived">True when the template was auto-derived to back a composition slot.</param>
|
||||
/// <param name="OwnerCompositionId">Owning composition row for a derived template.</param>
|
||||
/// <param name="AttributeCount">Number of directly declared attributes.</param>
|
||||
/// <param name="AlarmCount">Number of directly declared alarms.</param>
|
||||
/// <param name="ScriptCount">Number of directly declared scripts.</param>
|
||||
/// <param name="CompositionCount">Number of composition slots.</param>
|
||||
/// <param name="NativeAlarmSourceCount">Number of native alarm source bindings.</param>
|
||||
public record TemplateSummary(
|
||||
int Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
int? ParentTemplateId,
|
||||
int? FolderId,
|
||||
bool IsDerived,
|
||||
int? OwnerCompositionId,
|
||||
int AttributeCount,
|
||||
int AlarmCount,
|
||||
int ScriptCount,
|
||||
int CompositionCount,
|
||||
int NativeAlarmSourceCount)
|
||||
{
|
||||
/// <summary>
|
||||
/// Hard ceiling applied to a requested page size, mirroring the existing
|
||||
/// in-memory <c>Page</c> clamp in the management actor so DB-side paging
|
||||
/// cannot be used to pull an unbounded result set.
|
||||
/// </summary>
|
||||
public const int MaxPageSize = 1000;
|
||||
}
|
||||
Reference in New Issue
Block a user