test(historian): cancel the retention purge only once its DELETE is observed blocked (L6 review)

This commit is contained in:
Joseph Doherty
2026-10-11 10:57:44 -04:00
parent 1375f9349e
commit 69190b14f1
@@ -195,16 +195,33 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Sample WITH (TABLOCKX, HOLDLOCK)";
await cmd.ExecuteScalarAsync();
}
short holderSpid;
await using (var cmd = holder.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = "SELECT CAST(@@SPID AS smallint)";
holderSpid = (short)(await cmd.ExecuteScalarAsync())!;
}
using var cts = new CancellationTokenSource();
var purger = new SqlServerHistorianRetention(c, b, TimeSpan.FromHours(1), 30, 4000, time: new FixedTime(Now));
var purge = purger.PurgeAsync(cts.Token);
await Task.Delay(500);
purge.IsCompleted.ShouldBeFalse("the DELETE must be blocked behind the holder's table lock");
// Cancel only once the server reports a DELETE actually blocked behind the holder — a fixed delay could
// cancel during bootstrap/applock acquisition on a slow host and never exercise the mid-command path.
var deadline = DateTime.UtcNow.AddSeconds(15);
while (await CountAsync(
$"SELECT COUNT(*) FROM sys.dm_exec_requests WHERE blocking_session_id = {holderSpid} AND command = 'DELETE'") == 0)
{
purge.IsCompleted.ShouldBeFalse("the purge finished without ever blocking on the holder's table lock");
DateTime.UtcNow.ShouldBeLessThan(deadline, "the purge's DELETE never blocked behind the holder");
await Task.Delay(50);
}
cts.Cancel();
// SqlClient may surface the attention as a SqlException rather than OperationCanceledException.
await Should.ThrowAsync<Exception>(() => purge);
// SqlClient surfaces the attention as OperationCanceledException or as a SqlException.
var ex = await Should.ThrowAsync<Exception>(() => purge);
(ex is OperationCanceledException or SqlException).ShouldBeTrue($"unexpected {ex.GetType().Name}: {ex.Message}");
await tx.RollbackAsync();
(await LockIsFreeAsync(s)).ShouldBeTrue();