86 lines
3.3 KiB
C#
86 lines
3.3 KiB
C#
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);
|
||
}
|
||
}
|