fix(historian): retention applock release hardening, shutdown-quiet cancellation, release tests, docs (#667 review)

This commit is contained in:
Joseph Doherty
2026-10-11 08:27:31 -04:00
parent 924eff8d3c
commit e4123ada91
4 changed files with 69 additions and 15 deletions
@@ -36,20 +36,23 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
public TimeSpan Interval { get; }
/// <summary>Test hook invoked after every delete batch.</summary>
internal Action? AfterBatch { get; set; }
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);
if (!await TryLockAsync(conn, ct).ConfigureAwait(false))
{
_log.LogDebug("Historian retention purge skipped: another purger holds the applock.");
return [];
}
try
{
if (!await TryLockAsync(conn, ct).ConfigureAwait(false))
{
_log.LogInformation("Historian retention purge skipped: another purger holds the applock.");
return [];
}
long samples = 0;
var tagIds = new List<int>();
await using (var cmd = _conn.CreateCommand(conn, $"SELECT TagId FROM {_conn.Q("Tag")}"))
@@ -71,6 +74,7 @@ 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);
}
}
@@ -87,6 +91,7 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
SqlHistorianConnection.AddDateTime2(cmd, "@cutoff", cutoff);
var n = await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
total += n;
AfterBatch?.Invoke();
if (n < _batch) return total;
}
}
@@ -112,7 +117,11 @@ internal sealed class SqlServerHistorianRetention : IHistorianRetention
}
catch (Exception ex)
{
_log.LogDebug(ex, "Releasing the retention applock failed.");
// A pooled connection keeps its server session (sp_reset_connection does not release a Session-owned
// 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);
}
}
}