123 lines
4.6 KiB
C#
123 lines
4.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
|
|
|
|
/// <summary>
|
|
/// Central hosted service that purges <c>AuditLogEntries</c> (the configuration audit trail)
|
|
/// older than <see cref="ConfigAuditOptions.RetentionDays"/>. A no-op while
|
|
/// <c>RetentionDays</c> is 0 (the default — retain indefinitely).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Deletes in bounded batches (<see cref="ConfigAuditOptions.PurgeBatchSize"/>, at most
|
|
/// <see cref="ConfigAuditOptions.MaxBatchesPerTick"/> per tick) so a first purge over a large
|
|
/// backlog is time-sliced across ticks instead of one long-running DELETE. Both central nodes may
|
|
/// run it; deleting an already-deleted row is harmless, so no singleton is needed. A failed tick
|
|
/// is logged and retried on the next.
|
|
/// </remarks>
|
|
public sealed class ConfigAuditPurgeService : BackgroundService
|
|
{
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IOptions<ConfigAuditOptions> _options;
|
|
private readonly TimeProvider _time;
|
|
private readonly ILogger<ConfigAuditPurgeService> _logger;
|
|
|
|
/// <summary>Initializes the purge service.</summary>
|
|
/// <param name="scopeFactory">Creates a DI scope per tick (the DbContext is scoped).</param>
|
|
/// <param name="options">Retention options.</param>
|
|
/// <param name="time">Clock.</param>
|
|
/// <param name="logger">Logger.</param>
|
|
public ConfigAuditPurgeService(
|
|
IServiceScopeFactory scopeFactory,
|
|
IOptions<ConfigAuditOptions> options,
|
|
TimeProvider time,
|
|
ILogger<ConfigAuditPurgeService> logger)
|
|
{
|
|
_scopeFactory = scopeFactory;
|
|
_options = options;
|
|
_time = time;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
var opts = _options.Value;
|
|
if (opts.RetentionDays <= 0)
|
|
{
|
|
_logger.LogInformation("Config audit retention is off (RetentionDays=0); entries are kept indefinitely.");
|
|
return;
|
|
}
|
|
|
|
using var timer = new PeriodicTimer(opts.PurgeInterval, _time);
|
|
do
|
|
{
|
|
try
|
|
{
|
|
await PurgeOnceAsync(stoppingToken);
|
|
}
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Config audit purge tick failed; will retry next interval.");
|
|
}
|
|
}
|
|
while (await WaitNextAsync(timer, stoppingToken));
|
|
}
|
|
|
|
private static async Task<bool> WaitNextAsync(PeriodicTimer timer, CancellationToken ct)
|
|
{
|
|
try { return await timer.WaitForNextTickAsync(ct); }
|
|
catch (OperationCanceledException) { return false; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs one purge pass. Returns the number of rows deleted. Exposed for tests.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Total rows deleted this pass.</returns>
|
|
public async Task<int> PurgeOnceAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var opts = _options.Value;
|
|
if (opts.RetentionDays <= 0)
|
|
return 0;
|
|
|
|
var cutoff = _time.GetUtcNow().AddDays(-opts.RetentionDays);
|
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
|
|
var total = 0;
|
|
for (var batch = 0; batch < opts.MaxBatchesPerTick; batch++)
|
|
{
|
|
// Oldest-first, bounded: select the batch's keys then delete by key so the DELETE
|
|
// stays a short, index-ordered operation on SQL Server.
|
|
var ids = await db.AuditLogEntries
|
|
.AsNoTracking()
|
|
.Where(a => a.Timestamp < cutoff)
|
|
.OrderBy(a => a.Timestamp)
|
|
.Select(a => a.Id)
|
|
.Take(opts.PurgeBatchSize)
|
|
.ToListAsync(cancellationToken);
|
|
if (ids.Count == 0)
|
|
break;
|
|
|
|
total += await db.AuditLogEntries
|
|
.Where(a => ids.Contains(a.Id))
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
if (ids.Count < opts.PurgeBatchSize)
|
|
break;
|
|
}
|
|
|
|
if (total > 0)
|
|
_logger.LogInformation("Config audit purge removed {Count} entries older than {Cutoff:o}.", total, cutoff);
|
|
return total;
|
|
}
|
|
}
|