perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
@@ -10,21 +12,70 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
/// deployed snapshot to decide staleness. Hosted in DeploymentManager so the
|
||||
/// Transport component (which references only Commons + TemplateEngine) can probe
|
||||
/// staleness without taking a DeploymentManager project reference.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Watermark fast path.</b> A bundle import probes EVERY instance derived from
|
||||
/// an overwritten template, and the Deployments page sweeps the fleet — both pay a
|
||||
/// full flatten per instance for an answer that is usually "unchanged". A probe
|
||||
/// result is therefore memoised against the
|
||||
/// <see cref="ITemplateGraphWatermark"/> reading that produced it: the instance's
|
||||
/// own version, the graph's structure version (which covers chain MEMBERSHIP), and
|
||||
/// the version of every template the flatten actually walked. When all of those are
|
||||
/// unchanged the recorded hash is still current by construction and the flatten is
|
||||
/// skipped.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The fast path can only ever cause EXTRA work, never stale work: the watermark is
|
||||
/// in-memory and process-local, so a restart, a failover to the other central node,
|
||||
/// or any mutation at all simply misses and falls back to the authoritative
|
||||
/// flatten.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class StaleInstanceProbe : IStaleInstanceProbe
|
||||
{
|
||||
/// <summary>
|
||||
/// Ceiling on memoised probe results. Bounded because the key space is the
|
||||
/// instance table and this cache is process-wide and static; on overflow it is
|
||||
/// dropped wholesale, which is safe here (unlike the script-compile verdict
|
||||
/// cache) because a miss costs one flatten and leaks nothing.
|
||||
/// </summary>
|
||||
private const int MaxCachedProbes = 4096;
|
||||
|
||||
private static readonly ConcurrentDictionary<int, ProbeMemo> Memos = new();
|
||||
|
||||
private readonly IFlatteningPipeline _flatteningPipeline;
|
||||
private readonly ITemplateGraphWatermark _watermark;
|
||||
|
||||
/// <summary>Initializes a new <see cref="StaleInstanceProbe"/>.</summary>
|
||||
/// <param name="flatteningPipeline">The deployment flattening pipeline used to recompute the current revision hash.</param>
|
||||
public StaleInstanceProbe(IFlatteningPipeline flatteningPipeline)
|
||||
/// <param name="watermark">Graph version watermark backing the fast path.</param>
|
||||
public StaleInstanceProbe(IFlatteningPipeline flatteningPipeline, ITemplateGraphWatermark watermark)
|
||||
{
|
||||
_flatteningPipeline = flatteningPipeline ?? throw new ArgumentNullException(nameof(flatteningPipeline));
|
||||
_watermark = watermark ?? throw new ArgumentNullException(nameof(watermark));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A previously computed revision hash together with the exact watermark
|
||||
/// readings it was computed under.
|
||||
/// </summary>
|
||||
/// <param name="Structure">Graph structure version at flatten time.</param>
|
||||
/// <param name="InstanceVersion">The instance's own version at flatten time.</param>
|
||||
/// <param name="TemplateVersions">Version of every template the flatten walked, by template id.</param>
|
||||
/// <param name="RevisionHash">The hash that reading produced.</param>
|
||||
private sealed record ProbeMemo(
|
||||
long Structure,
|
||||
long InstanceVersion,
|
||||
IReadOnlyDictionary<int, long> TemplateVersions,
|
||||
string RevisionHash);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetCurrentRevisionHashAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (TryGetMemoisedHash(instanceId, out var memoised))
|
||||
return memoised;
|
||||
|
||||
// The pipeline returns a Result; a flatten failure (e.g. unresolvable
|
||||
// template chain mid-import) yields null so the caller treats the
|
||||
// instance as "staleness indeterminate" and skips it. Flattening reuses
|
||||
@@ -33,9 +84,63 @@ public sealed class StaleInstanceProbe : IStaleInstanceProbe
|
||||
// Staleness only needs the revision hash — skip the expensive Roslyn
|
||||
// script-compilation stage (this probe is called per-instance across a whole
|
||||
// bundle import in BundleImporter.ComputeStaleInstanceIdsAsync).
|
||||
//
|
||||
// Watermark readings are taken BEFORE the flatten. A mutation that lands
|
||||
// during the flatten therefore bumps a version past the recorded one and
|
||||
// invalidates the memo on the next read, rather than being captured as if
|
||||
// it had already been included.
|
||||
var structure = _watermark.StructureVersion;
|
||||
var instanceVersion = _watermark.GetInstanceVersion(instanceId);
|
||||
|
||||
var session = _flatteningPipeline.CreateSession();
|
||||
var result = await _flatteningPipeline
|
||||
.FlattenAndValidateAsync(instanceId, cancellationToken, validateScripts: false)
|
||||
.FlattenAndValidateAsync(instanceId, cancellationToken, validateScripts: false, session)
|
||||
.ConfigureAwait(false);
|
||||
return result.IsSuccess ? result.Value.RevisionHash : null;
|
||||
|
||||
if (result.IsFailure)
|
||||
return null;
|
||||
|
||||
if (Memos.Count >= MaxCachedProbes)
|
||||
Memos.Clear();
|
||||
|
||||
var visited = session.VisitedTemplateIds;
|
||||
var versions = new Dictionary<int, long>(visited.Count);
|
||||
foreach (var templateId in visited)
|
||||
versions[templateId] = _watermark.GetTemplateVersion(templateId);
|
||||
|
||||
Memos[instanceId] = new ProbeMemo(structure, instanceVersion, versions, result.Value.RevisionHash);
|
||||
return result.Value.RevisionHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the memoised revision hash when every watermark reading it was
|
||||
/// computed under is still current.
|
||||
/// </summary>
|
||||
private bool TryGetMemoisedHash(int instanceId, out string? hash)
|
||||
{
|
||||
hash = null;
|
||||
if (!Memos.TryGetValue(instanceId, out var memo))
|
||||
return false;
|
||||
|
||||
if (memo.Structure != _watermark.StructureVersion)
|
||||
return false;
|
||||
|
||||
if (memo.InstanceVersion != _watermark.GetInstanceVersion(instanceId))
|
||||
return false;
|
||||
|
||||
foreach (var (templateId, version) in memo.TemplateVersions)
|
||||
{
|
||||
if (_watermark.GetTemplateVersion(templateId) != version)
|
||||
return false;
|
||||
}
|
||||
|
||||
hash = memo.RevisionHash;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops every memoised probe result. Test hook — production invalidation is
|
||||
/// driven entirely by the watermark.
|
||||
/// </summary>
|
||||
public static void ClearMemos() => Memos.Clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user