test(host): red repro — apply-boundary cache clear races in-flight VT evaluation into ObjectDisposedException (02/S12)
This commit is contained in:
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"id": "R2-03-T7",
|
||||
"subject": "S12 concurrent stress repro (RED): 300 compiled evaluations racing ClearCompiledScripts, WaitAsync-bounded, assert zero disposed-reason failures",
|
||||
"status": "pending",
|
||||
"status": "completed",
|
||||
"blockedBy": []
|
||||
},
|
||||
{
|
||||
|
||||
+58
@@ -380,6 +380,64 @@ public sealed class RoslynVirtualTagEvaluatorTests
|
||||
CompiledCacheCount(sut).ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>02/S12 concurrent stress repro (RED on current code): while one loop runs many compiled
|
||||
/// (non-passthrough) evaluations of the same expression, a second loop hammers
|
||||
/// <see cref="RoslynVirtualTagEvaluator.ClearCompiledScripts"/> — the apply-boundary clear. On the
|
||||
/// unfixed tree a clear landing between a child's <c>GetOrCompile</c> and the run's disposed-guard
|
||||
/// surfaces an ObjectDisposedException as a <c>Failure("script threw: Cannot access a disposed
|
||||
/// object…")</c>; the fix retries once and every result stays Success. The whole test is WaitAsync-
|
||||
/// bounded so a regression fails fast rather than hanging.</summary>
|
||||
[Fact]
|
||||
public async Task Evaluate_racing_ClearCompiledScripts_never_fails_with_disposed()
|
||||
{
|
||||
// Generous timeout so the wall-clock budget never fires and confounds the race assertion.
|
||||
using var sut = new RoslynVirtualTagEvaluator(
|
||||
NullLogger<RoslynVirtualTagEvaluator>.Instance, NoOpScriptRoot(), TimeSpan.FromSeconds(5));
|
||||
const string expr = "return (int)ctx.GetTag(\"a\").Value + 1;";
|
||||
|
||||
const int loopCount = 12;
|
||||
const int iterations = 400;
|
||||
var failures = new System.Collections.Concurrent.ConcurrentBag<string>();
|
||||
var remaining = loopCount; // number of concurrent evaluation loops still running
|
||||
|
||||
// Many concurrent evaluation loops put the thread pool under enough pressure that the inner
|
||||
// Task.Run hop inside TimedScriptEvaluator is measurably delayed — widening the
|
||||
// fetch→run-disposed-guard window so a clear reliably lands inside SOME in-flight evaluation.
|
||||
// The clear loop clears on a short spin cadence (not back-to-back) so (a) most evaluations hit
|
||||
// the warm cache — keeping compile cost bounded — and (b) at most one clear lands within a
|
||||
// single evaluation, so the single retry the fix adds is sufficient (a back-to-back clear loop
|
||||
// would both compile-swamp the RED run into a timeout and defeat the one-shot retry). Pre-fix a
|
||||
// clear disposing the just-fetched evaluator surfaces as Failure("script threw: Cannot access a
|
||||
// disposed object…"); post-fix the retry re-fetches into the current generation → all Success.
|
||||
var evalLoops = Enumerable.Range(0, loopCount).Select(_ => Task.Run(() =>
|
||||
{
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var r = sut.Evaluate("vt-race", expr, new Dictionary<string, object?> { ["a"] = i });
|
||||
if (!r.Success)
|
||||
{
|
||||
failures.Add(r.Reason ?? "<null reason>");
|
||||
}
|
||||
}
|
||||
Interlocked.Decrement(ref remaining);
|
||||
}, TestContext.Current.CancellationToken)).ToArray();
|
||||
|
||||
var clearLoop = Task.Run(() =>
|
||||
{
|
||||
while (Volatile.Read(ref remaining) > 0)
|
||||
{
|
||||
sut.ClearCompiledScripts();
|
||||
Thread.SpinWait(4000); // ~tens of µs between clears — frequent, but not back-to-back
|
||||
}
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
await Task.WhenAll(evalLoops.Append(clearLoop))
|
||||
.WaitAsync(TimeSpan.FromSeconds(45), TestContext.Current.CancellationToken);
|
||||
|
||||
failures.ShouldBeEmpty(
|
||||
$"every evaluation must succeed despite racing clears; observed: {string.Join(" | ", failures)}");
|
||||
}
|
||||
|
||||
/// <summary>Reads the count of the private compiled-script cache via reflection. The cache is a
|
||||
/// <c>CompiledScriptCache<,></c> which exposes a <c>Count</c> property (not <c>ICollection</c>).</summary>
|
||||
private static int CompiledCacheCount(RoslynVirtualTagEvaluator sut)
|
||||
|
||||
Reference in New Issue
Block a user