feat(audit): M5.5 per-channel retention overrides via purge-role bounded delete (T3)
This commit is contained in:
@@ -167,6 +167,9 @@ public class AuditLogPurgeActor : ReceiveActor
|
||||
|
||||
if (boundaries.Count == 0)
|
||||
{
|
||||
// No whole-month partitions are eligible, but per-channel overrides may
|
||||
// still expire rows earlier than the global window — run them below.
|
||||
await RunPerChannelOverridesAsync(repository).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +205,80 @@ public class AuditLogPurgeActor : ReceiveActor
|
||||
sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// M5.5 (T3): after the channel-blind global partition switch-out, apply any
|
||||
// per-channel retention overrides that are SHORTER than the global window via
|
||||
// a bounded, batched row DELETE on the same maintenance path. The global
|
||||
// switch-out has already dropped whole months older than RetentionDays; these
|
||||
// deletes only ever expire rows EARLIER than that, so they run last and are a
|
||||
// strict tightening.
|
||||
await RunPerChannelOverridesAsync(repository).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M5.5 (T3): runs each per-channel retention override whose window is strictly
|
||||
/// shorter than the global <see cref="AuditLogOptions.RetentionDays"/>, deleting
|
||||
/// rows of that channel older than the channel-specific threshold via a bounded,
|
||||
/// batched maintenance-path DELETE. Each channel runs inside its own try/catch so
|
||||
/// one bad channel does not abandon the others on the same tick, mirroring the
|
||||
/// per-boundary error isolation of the partition switch-out loop.
|
||||
/// </summary>
|
||||
/// <param name="repository">The repository resolved for this tick's DI scope.</param>
|
||||
private async Task RunPerChannelOverridesAsync(IAuditLogRepository repository)
|
||||
{
|
||||
var overrides = _auditOptions.PerChannelRetentionDays;
|
||||
if (overrides is null || overrides.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var globalDays = _auditOptions.RetentionDays;
|
||||
|
||||
foreach (var (channel, days) in overrides)
|
||||
{
|
||||
// Only act when the per-channel window is strictly shorter than the global
|
||||
// one. Equal/longer windows are already covered by the global partition
|
||||
// switch-out, so a row DELETE would be redundant work (and a longer window
|
||||
// is meaningless — the partition is dropped on the global schedule).
|
||||
if (days >= globalDays)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var channelThreshold = DateTime.UtcNow - TimeSpan.FromDays(days);
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var rowsDeleted = await repository
|
||||
.PurgeChannelOlderThanAsync(channel, channelThreshold, _purgeOptions.ChannelPurgeBatchSize)
|
||||
.ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
if (rowsDeleted > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Purged {RowsDeleted} AuditLog rows for channel {Channel} older than {Threshold:o} " +
|
||||
"(per-channel override {Days}d < global {GlobalDays}d) in {DurationMs} ms.",
|
||||
rowsDeleted,
|
||||
channel,
|
||||
channelThreshold,
|
||||
days,
|
||||
globalDays,
|
||||
sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Failed to apply per-channel retention override for channel {Channel} " +
|
||||
"({Days}d); other channels continue. Elapsed {DurationMs} ms.",
|
||||
channel,
|
||||
days,
|
||||
sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Self-tick triggering a purge pass across all eligible partitions.</summary>
|
||||
|
||||
@@ -28,6 +28,24 @@ public sealed class AuditLogPurgeOptions
|
||||
/// <summary>Period of the purge tick in hours (default 24).</summary>
|
||||
public int IntervalHours { get; set; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// M5.5 (T3): batch size for the per-channel retention-override row DELETE
|
||||
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.PurgeChannelOlderThanAsync"/>).
|
||||
/// Each <c>DELETE TOP (@batch)</c> caps the transaction-log and lock footprint
|
||||
/// per statement; the repository loops batches until no rows remain. Default
|
||||
/// 5000 keeps individual deletes short on a busy central DB while still draining
|
||||
/// a large backlog within a tick. Clamped to a sane minimum in
|
||||
/// <see cref="ChannelPurgeBatchSize"/>.
|
||||
/// </summary>
|
||||
public int ChannelPurgeBatchSizeConfigured { get; set; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective per-channel purge batch size, clamped to at least 1 so
|
||||
/// a misconfigured <c>0</c>/negative value cannot make the repository's DELETE
|
||||
/// loop spin or throw.
|
||||
/// </summary>
|
||||
public int ChannelPurgeBatchSize => ChannelPurgeBatchSizeConfigured < 1 ? 1 : ChannelPurgeBatchSizeConfigured;
|
||||
|
||||
/// <summary>
|
||||
/// Test-only override for finer control over the tick cadence than
|
||||
/// whole-hour resolution allows. When non-null, takes precedence over
|
||||
|
||||
Reference in New Issue
Block a user