test(siteeventlog): make P4-5's ExecuteAsync-yield test deterministic — park the purge on the isActiveNode gate instead of racing the pool thread
The T4 test asserted 'reflection Invoke returned AND the row is still present', but the yielded continuation runs RunPurge on a pool thread and can delete the row before the test thread reaches its assert — lost 2/2 full-suite sweeps on the integration branch, green in isolation (row-40 class, introduced by this wave). The purge now blocks inside the isActiveNode check (consulted before any delete) until the test releases it: fixed code returns from Invoke while the gate is held; unfixed code blocks on the gate itself and the bounded wait fails. Proof of failure re-run: with Task.Yield() removed the test fails with the intended message after the 10 s bound; 3/3 green with it restored.
This commit is contained in:
@@ -381,22 +381,39 @@ public class EventLogPurgeServiceTests : IDisposable
|
|||||||
// expired row before this method's Assert ever executes. With Task.Yield() as the
|
// expired row before this method's Assert ever executes. With Task.Yield() as the
|
||||||
// very first statement, Invoke returns as soon as MoveNext() hits the (always
|
// very first statement, Invoke returns as soon as MoveNext() hits the (always
|
||||||
// IsCompleted == false) yield point, before RunPurge has been called at all.
|
// IsCompleted == false) yield point, before RunPurge has been called at all.
|
||||||
|
//
|
||||||
|
// Made deterministic at integration (2026-08-20): "Invoke returned, row still present"
|
||||||
|
// on its own is a RACE — the yielded continuation runs RunPurge on a pool thread and
|
||||||
|
// can delete the row before this thread reaches its assert (lost 2/2 full-suite
|
||||||
|
// sweeps, green in isolation). So the purge is parked on a gate instead: RunPurge
|
||||||
|
// consults the isActiveNode check BEFORE any delete, and the check blocks until the
|
||||||
|
// test releases it. Fixed code: Invoke returns while the gate is held (the continuation
|
||||||
|
// is the one waiting). Unfixed code: Invoke itself blocks on the gate, so the
|
||||||
|
// bounded wait below fails — no timing assumption in either direction.
|
||||||
InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-31));
|
InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-31));
|
||||||
Assert.Equal(1, GetEventCount());
|
Assert.Equal(1, GetEventCount());
|
||||||
|
|
||||||
var purge = CreatePurgeService();
|
using var gate = new ManualResetEventSlim(initialState: false);
|
||||||
|
var purge = CreatePurgeService(isActiveNode: () => { gate.Wait(); return true; });
|
||||||
var executeAsync = typeof(EventLogPurgeService).GetMethod(
|
var executeAsync = typeof(EventLogPurgeService).GetMethod(
|
||||||
"ExecuteAsync", BindingFlags.NonPublic | BindingFlags.Instance);
|
"ExecuteAsync", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
Assert.NotNull(executeAsync);
|
Assert.NotNull(executeAsync);
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
var task = (Task)executeAsync!.Invoke(purge, [cts.Token])!;
|
Task? executeTask = null;
|
||||||
|
var invoke = Task.Factory.StartNew(
|
||||||
|
() => executeTask = (Task)executeAsync!.Invoke(purge, [cts.Token])!,
|
||||||
|
TaskCreationOptions.LongRunning);
|
||||||
|
|
||||||
// Reflection Invoke has already returned control here — a synchronous
|
// With the gate held, Invoke can only return if ExecuteAsync yielded before RunPurge.
|
||||||
// RunPurge() ahead of any await would have already deleted the row by now.
|
var returned = await Task.WhenAny(invoke, Task.Delay(TimeSpan.FromSeconds(10))) == invoke;
|
||||||
|
Assert.True(returned,
|
||||||
|
"ExecuteAsync did not return control before running the initial purge — " +
|
||||||
|
"it is running RunPurge synchronously ahead of its first await.");
|
||||||
Assert.Equal(1, GetEventCount());
|
Assert.Equal(1, GetEventCount());
|
||||||
|
|
||||||
// The initial purge still runs, just after control has been yielded back.
|
// Release the purge; it still runs, just after control was yielded back.
|
||||||
|
gate.Set();
|
||||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||||
while (GetEventCount() != 0 && DateTime.UtcNow < deadline)
|
while (GetEventCount() != 0 && DateTime.UtcNow < deadline)
|
||||||
{
|
{
|
||||||
@@ -407,7 +424,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
|||||||
await cts.CancelAsync();
|
await cts.CancelAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task;
|
await executeTask!;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user