refactor(siteruntime): scope SiteScriptCompileCache to the ScriptCompilationService singleton — close register row 100
The compile cache was a `static` class, so its entry table and its Hits/Count
counters were one process-global blob every test class in the SiteRuntime
assembly wrote into. Row 91 last wave neutered the resulting flakes (per-test
GUID keys + one-sided counter bounds), but the shared mutable state itself
remained: any test calling Clear() evicted entries other parallel classes had
just inserted, and a Clear() from a class outside the serialized collection
would degrade the others to recompiles and re-open the row-91 flake class.
PREMISE CORRECTION: the register speculated production clears "per-instance or
per-deployment script entries" and offered ClearKeys(prefix) as the natural fit.
There are NO production callers of Clear() at all — grep of src/ finds only
GetOrAdd (ScriptCompilationService.CompileCore). Clear() existed solely so
tests could reset the global. A key-scoped API would therefore have been new
production surface built for nobody, so the instance-scoped seam is the fix and
Clear() is deleted outright rather than migrated.
- SiteScriptCompileCache: `internal static class` -> `internal sealed class`;
Cache/EvictionLock/_hits/_accessSequence become instance fields. LRU batch
eviction, the double-checked sweep and the ConcurrentDictionary.ToArray()
snapshot are byte-for-byte the same policy. Clear() removed.
- ScriptCompilationService owns one cache. Public ctor (the DI path) builds its
own; a new internal ctor takes one for tests. Production N4 sharing is
unchanged and now rests on the DI SINGLETON registration — the one service
instance is threaded to DeployCompileValidator and to every Instance Actor —
so that lifetime is load-bearing and is pinned by a new registration test.
- Tests: all three Clear() callers migrated off it. Each test now owns its cache,
which lets the assertions get STRONGER — the row-91 workarounds are retired:
Hits/Count are exact again (no >= baseline bounds), the per-test GUID key
token is gone, and ForeignEntrySlack (512 entries of allowance on the bound)
drops to the racing-thread count in the concurrency test. New test
SeparateCaches_DoNotShareEntriesOrCounters pins the scoping directly: filling
one cache past MaxEntries leaves another's entry, hit count and identity
intact. DeploymentWarmThenGateTests additionally asserts Count == 1 now that
its counter is private.
- The [Collection("SiteScriptCompileCache")] serialization from row 91 is
RELAXED (removed from all three classes) — they no longer touch any shared
state, so there is nothing left to serialize.
- Docs: Component-SiteRuntime.md gains the ownership/singleton note and drops
the Clear() reference; DeploymentManagerActor / DeployCompileValidator /
ServiceCollectionExtensions comments corrected to say where the cache lives.
Build: 0 warnings. SiteRuntime.Tests 643/643, three consecutive full-parallel
runs. AuditLog.Tests correlation classes (other ScriptCompilationService
consumers) green.
This commit is contained in:
@@ -14,22 +14,48 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
/// execution-path compile of the script against <see cref="ScriptGlobals"/> /
|
||||
/// <see cref="TriggerExpressionGlobals"/>.
|
||||
///
|
||||
/// Compile results — success AND failure — are memoised process-wide in
|
||||
/// Compile results — success AND failure — are memoised in this service's
|
||||
/// <see cref="SiteScriptCompileCache"/> (keyed on code hash + globals type), so an
|
||||
/// unchanged script body compiles once per process lifetime and the deploy gate,
|
||||
/// the Instance Actor's PreStart recompile, and SharedScriptLibrary all reuse one
|
||||
/// compiled <c>Script<object?></c>. Error text is name-free by construction, so a
|
||||
/// cached failure renders correctly for every caller.
|
||||
///
|
||||
/// <para><b>The cache is owned per service instance, not static (register row 100).</b>
|
||||
/// "Once per process" is a property of this service being registered as a DI
|
||||
/// <b>singleton</b> (<see cref="ServiceCollectionExtensions.AddSiteRuntime"/>, pinned by
|
||||
/// <c>SiteRuntimeServiceRegistrationTests</c>) — the whole site reaches the cache through
|
||||
/// that one instance, which the Deployment Manager threads into its
|
||||
/// <see cref="Deployment.DeployCompileValidator"/> and into every Instance Actor. Making the
|
||||
/// cache an instance field rather than static state means a test (or a second host in one
|
||||
/// process) that constructs its own service gets its own cache instead of mutating everyone
|
||||
/// else's; that shared-static cross-talk is what row 100 closed.</para>
|
||||
/// </summary>
|
||||
public class ScriptCompilationService
|
||||
{
|
||||
private readonly ILogger<ScriptCompilationService> _logger;
|
||||
private readonly SiteScriptCompileCache _compileCache;
|
||||
|
||||
/// <summary>Initializes a new instance of the ScriptCompilationService class.</summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public ScriptCompilationService(ILogger<ScriptCompilationService> logger)
|
||||
: this(logger, new SiteScriptCompileCache())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test seam: constructs the service over a caller-supplied compile cache, so a test can
|
||||
/// observe <see cref="SiteScriptCompileCache.Hits"/> / <see cref="SiteScriptCompileCache.Count"/>
|
||||
/// for exactly the compiles it caused. Production always uses the public constructor.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="compileCache">The compile cache this service memoises into.</param>
|
||||
internal ScriptCompilationService(
|
||||
ILogger<ScriptCompilationService> logger,
|
||||
SiteScriptCompileCache compileCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_compileCache = compileCache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,16 +162,16 @@ public class ScriptCompilationService
|
||||
/// <summary>
|
||||
/// Shared compilation funnel for every site-side compile (the deploy gate, the
|
||||
/// Instance Actor's PreStart recompile, and SharedScriptLibrary). Results — success
|
||||
/// AND failure — are memoised process-wide in <see cref="SiteScriptCompileCache"/>,
|
||||
/// AND failure — are memoised in this service's <see cref="SiteScriptCompileCache"/>,
|
||||
/// keyed on code hash + globals type: an unchanged script body compiles once per
|
||||
/// process lifetime and the compiled <c>Script<object?></c> is shared across all
|
||||
/// call sites (N4). Cached error text is name-free by construction (Roslyn diagnostics /
|
||||
/// the compile-exception message never embed the caller's script name), so a cached
|
||||
/// failure renders correctly for every caller.
|
||||
/// process lifetime (this service is a DI singleton) and the compiled
|
||||
/// <c>Script<object?></c> is shared across all call sites (N4). Cached error text is
|
||||
/// name-free by construction (Roslyn diagnostics / the compile-exception message never
|
||||
/// embed the caller's script name), so a cached failure renders correctly for every caller.
|
||||
/// </summary>
|
||||
private ScriptCompilationResult CompileCore(string name, string code, Type globalsType)
|
||||
{
|
||||
var result = SiteScriptCompileCache.GetOrAdd(code, globalsType,
|
||||
var result = _compileCache.GetOrAdd(code, globalsType,
|
||||
() => CompileUncached(name, code, globalsType));
|
||||
if (!result.IsSuccess)
|
||||
_logger.LogDebug(
|
||||
|
||||
Reference in New Issue
Block a user