Merge fix/667-followups: release the retention applock only when acquired (#667 live-gate F3)

This commit is contained in:
Joseph Doherty
2026-10-11 09:13:30 -04:00
2 changed files with 63 additions and 4 deletions
@@ -171,4 +171,46 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
await Should.ThrowAsync<InvalidOperationException>(() => purger.PurgeAsync(CancellationToken.None));
(await LockIsFreeAsync(s)).ShouldBeTrue();
}
private sealed class CapturingLogger : Microsoft.Extensions.Logging.ILogger
{
public readonly List<Microsoft.Extensions.Logging.LogLevel> Levels = [];
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true;
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId,
TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
lock (Levels) Levels.Add(logLevel);
}
}
[Fact]
public async Task A_skipped_pass_logs_no_warning_and_does_not_clear_the_pool()
{
fx.SkipIfUnavailable();
var (c, b, _, s) = Make();
await SeedAsync(b, s);
await using var holder = new SqlConnection(fx.ConnectionString);
await holder.OpenAsync();
await using (var cmd = holder.CreateCommand())
{
cmd.CommandText = $"EXEC sp_getapplock @Resource = N'OtOpcUa.Historian.Retention.{s}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = 0;";
await cmd.ExecuteNonQueryAsync();
}
var log = new CapturingLogger();
var clears = 0;
var purger = new SqlServerHistorianRetention(c, b, TimeSpan.FromHours(1), 30, 4000, log, new FixedTime(Now))
{
ClearPool = _ => Interlocked.Increment(ref clears),
};
for (var i = 0; i < 3; i++) (await purger.PurgeAsync(CancellationToken.None)).ShouldBeEmpty();
lock (log.Levels) log.Levels.ShouldNotContain(l => l >= Microsoft.Extensions.Logging.LogLevel.Warning);
clears.ShouldBe(0);
// The shared pool is still healthy.
(await CountAsync($"SELECT COUNT(*) FROM [{s}].Sample")).ShouldBe(21);
}
}