fix(historian): release the retention applock only when acquired (#667 F3)

This commit is contained in:
Joseph Doherty
2026-10-11 09:13:23 -04:00
parent db6446ce73
commit e583b068a1
2 changed files with 63 additions and 4 deletions
@@ -39,15 +39,31 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
/// <summary>Test hook invoked after every delete batch.</summary>
internal Action? AfterBatch { get; set; }
/// <summary>Test seam for <see cref="SqlConnection.ClearPool"/>.</summary>
internal Action<SqlConnection> ClearPool { get; set; } = SqlConnection.ClearPool;
public async Task<IReadOnlyList<RetentionPurgeResult>> PurgeAsync(CancellationToken ct)
{
await _bootstrap.EnsureAsync(ct).ConfigureAwait(false);
var cutoff = _time.GetUtcNow().UtcDateTime.AddDays(-_days);
await using var conn = await _conn.OpenAsync(ct).ConfigureAwait(false);
var acquired = false;
try
{
if (!await TryLockAsync(conn, ct).ConfigureAwait(false))
try
{
acquired = await TryLockAsync(conn, ct).ConfigureAwait(false);
}
catch
{
// Ambiguous: the server may have granted the lock before the client saw the reply. We cannot
// release what we are not sure we hold, so close the session physically instead.
ClearPool(conn);
throw;
}
if (!acquired)
{
_log.LogInformation("Historian retention purge skipped: another purger holds the applock.");
return [];
@@ -74,8 +90,9 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
}
finally
{
// Also runs when acquire threw: the server may have granted the lock before the client saw the reply.
await ReleaseAsync(conn).ConfigureAwait(false);
// Only when WE hold it: releasing an applock we were refused fails, which would log a false Warning
// and clear the shared pool on every skipped interval.
if (acquired) await ReleaseAsync(conn).ConfigureAwait(false);
}
}
@@ -121,7 +138,7 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
// applock); clearing the pool makes the dispose physically close it, which releases the lock.
_log.LogWarning("sp_releaseapplock failed ({Exception}); discarding the connection pool so the session " +
"holding the retention applock is closed rather than pooled.", ex.GetType().Name);
SqlConnection.ClearPool(conn);
ClearPool(conn);
}
}
}