Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/TrustVerdictCache.cs
T

86 lines
3.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
/// <summary>
/// Process-wide memo of <see cref="ScriptTrustValidator"/> verdicts for the
/// default policy reference set, keyed on the SHA-256 of the script code (review
/// 2026-10-21 04/P3).
///
/// <para>
/// A verdict is a pure function of (code, <see cref="ScriptTrustPolicy"/>,
/// <see cref="ScriptTrustPolicy.AnalysisReferences"/>), and the policy and
/// reference set are process-static, so the code hash alone identifies it. Each
/// miss is a full-framework semantic analysis (~170 references) — the bundle
/// importer runs the same script set through it on preview and again on apply,
/// and the wizard re-enters preview — so memoising here removes that repeat cost
/// without any change at the call sites.
/// </para>
///
/// <para>
/// Bounded like the Template Engine's compile-verdict cache: two generations of
/// <see cref="SegmentCapacity"/> entries; a hit in the cold generation is promoted,
/// and overflow drops only the cold half nothing touched for a whole generation.
/// Only completed verdicts are stored — an analysis that throws leaves the slot
/// empty. Stored verdicts are read-only collections, so a caller cannot mutate a
/// shared entry.
/// </para>
/// </summary>
internal static class TrustVerdictCache
{
/// <summary>Upper bound on entries per generation (2 × this in total).</summary>
internal const int SegmentCapacity = 1024;
private static readonly object RotateGate = new();
private static volatile ConcurrentDictionary<string, IReadOnlyList<string>> _hot = new();
private static volatile ConcurrentDictionary<string, IReadOnlyList<string>> _cold = new();
/// <summary>
/// Returns the cached verdict for <paramref name="code"/>, or computes it with
/// <paramref name="analyse"/> and caches it on a miss.
/// </summary>
/// <param name="code">The script source.</param>
/// <param name="analyse">Computes the verdict on a miss; a throw is not cached.</param>
/// <returns>The (read-only) violation list.</returns>
internal static IReadOnlyList<string> GetOrAdd(string code, Func<IReadOnlyList<string>> analyse)
{
var key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code)));
var hot = _hot;
var cold = _cold;
if (hot.TryGetValue(key, out var verdict))
return verdict;
if (cold.TryGetValue(key, out verdict))
{
Store(key, verdict);
return verdict;
}
verdict = analyse().ToList().AsReadOnly();
Store(key, verdict);
return verdict;
}
private static void Store(string key, IReadOnlyList<string> verdict)
{
if (_hot.Count >= SegmentCapacity)
{
lock (RotateGate)
{
if (_hot.Count >= SegmentCapacity)
{
_cold = _hot;
_hot = new ConcurrentDictionary<string, IReadOnlyList<string>>();
}
}
}
// TryAdd, not an indexer write: a racing thread that computed the same verdict
// keeps the instance already published, so every reader shares one object.
_hot.TryAdd(key, verdict);
}
}