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
-12
@@ -15,15 +15,20 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
/// <c>System.Diagnostics.Process</c> is blocked under System.Diagnostics —
|
||||
/// <c>Stopwatch</c> stays allowed. The real execution-path compile against
|
||||
/// <c>ScriptGlobals</c> / <c>TriggerExpressionGlobals</c> is unchanged.
|
||||
///
|
||||
/// <para><b>Register row 100:</b> the compile cache is owned by the service instance, not
|
||||
/// static, so this class's service — and therefore its cache — is private to each test. The
|
||||
/// <c>[Collection("SiteScriptCompileCache")]</c> serialisation and the <c>Clear()</c> calls that
|
||||
/// used to open the cache-sensitive tests are gone; <c>Hits</c> is now exact.</para>
|
||||
/// </summary>
|
||||
[Collection("SiteScriptCompileCache")]
|
||||
public class ScriptCompilationServiceTests
|
||||
{
|
||||
private readonly SiteScriptCompileCache _cache = new();
|
||||
private readonly ScriptCompilationService _service;
|
||||
|
||||
public ScriptCompilationServiceTests()
|
||||
{
|
||||
_service = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
||||
_service = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance, _cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -38,28 +43,27 @@ public class ScriptCompilationServiceTests
|
||||
[Fact]
|
||||
public void Compile_SameCodeTwice_SharesOneRoslynCompile()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var r1 = _service.Compile("deploy-gate-copy", "return 1 + 1;");
|
||||
var r2 = _service.Compile("prestart-copy", "return 1 + 1;");
|
||||
|
||||
Assert.True(r1.IsSuccess);
|
||||
Assert.Same(r1.CompiledScript, r2.CompiledScript); // one compile, shared Script<T> (N4) — the definitive proof
|
||||
// Hits is a process-global counter; other test classes in this assembly compile scripts
|
||||
// concurrently (they are not in this serialized collection), so the exact post-Clear count
|
||||
// is not deterministic under a full-assembly parallel run. The shared-Script<T> assertion
|
||||
// above is the real proof of cache reuse; here we only require the second lookup registered
|
||||
// a hit (>= 1) rather than pinning an exact global count.
|
||||
Assert.True(SiteScriptCompileCache.Hits >= 1);
|
||||
// The cache belongs to this service instance (register row 100), so the hit count is
|
||||
// exactly the one the second Compile caused — it used to be a >= 1 lower bound on a
|
||||
// process-global counter other test classes were bumping in parallel.
|
||||
Assert.Equal(1, _cache.Hits);
|
||||
Assert.Equal(1, _cache.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_ScriptAndTriggerExpression_DoNotCrossContaminate()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var script = _service.Compile("s", "1 > 0");
|
||||
var trigger = _service.CompileTriggerExpression("t", "1 > 0");
|
||||
|
||||
Assert.NotSame(script.CompiledScript, trigger.CompiledScript); // different globals surfaces
|
||||
Assert.Equal(2, _cache.Count); // two entries, no cross-globals hit
|
||||
Assert.Equal(0, _cache.Hits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -231,7 +235,6 @@ public class ScriptCompilationServiceTests
|
||||
[Fact]
|
||||
public void Compile_DistinctScripts_ShareOneMetadataReferenceSet_SoNativeMemoryDoesNotGrow()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var first = _service.Compile("first", "return 1 + 1;");
|
||||
var second = _service.Compile("second", "return 2 + 2;");
|
||||
|
||||
@@ -255,7 +258,6 @@ public class ScriptCompilationServiceTests
|
||||
[Fact]
|
||||
public void Compile_UsesProcessWideCachingMetadataResolver()
|
||||
{
|
||||
SiteScriptCompileCache.Clear();
|
||||
var result = _service.Compile("resolver-pin", "return 41 + 1;");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
Reference in New Issue
Block a user