65 lines
2.6 KiB
C#
65 lines
2.6 KiB
C#
|
|
using Dapper;
|
||
|
|
using Microsoft.Extensions.Logging;
|
||
|
|
using Quartz;
|
||
|
|
using SIGCM2.Infrastructure.Persistence;
|
||
|
|
|
||
|
|
namespace SIGCM2.Infrastructure.Audit.Jobs;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// UDT-010 (#REQ-AUD-6) — monthly maintenance:
|
||
|
|
/// - Extends the forward boundary of pf_AuditEvent_Monthly and pf_SecurityEvent_Monthly
|
||
|
|
/// so the next month always has a partition ready (SPLIT RANGE).
|
||
|
|
/// - Intended schedule: cron '0 0 2 1 * ?' (day 1 each month at 02:00 UTC).
|
||
|
|
/// - Idempotent: only splits if the target boundary does not yet exist.
|
||
|
|
/// </summary>
|
||
|
|
[DisallowConcurrentExecution]
|
||
|
|
public sealed class AuditPartitionManagerJob : IJob
|
||
|
|
{
|
||
|
|
public const string CronSchedule = "0 0 2 1 * ?";
|
||
|
|
|
||
|
|
private readonly SqlConnectionFactory _factory;
|
||
|
|
private readonly ILogger<AuditPartitionManagerJob> _logger;
|
||
|
|
|
||
|
|
public AuditPartitionManagerJob(SqlConnectionFactory factory, ILogger<AuditPartitionManagerJob> logger)
|
||
|
|
{
|
||
|
|
_factory = factory;
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task Execute(IJobExecutionContext context)
|
||
|
|
{
|
||
|
|
var ct = context.CancellationToken;
|
||
|
|
await using var conn = _factory.CreateConnection();
|
||
|
|
await conn.OpenAsync(ct);
|
||
|
|
|
||
|
|
// Target: boundary for "next month + 1" (so the next month is always pre-created and we
|
||
|
|
// keep at least one boundary ahead after the rotation).
|
||
|
|
var now = DateTime.UtcNow;
|
||
|
|
var target = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2);
|
||
|
|
|
||
|
|
var affected = 0;
|
||
|
|
foreach (var pf in new[] { "pf_AuditEvent_Monthly", "pf_SecurityEvent_Monthly" })
|
||
|
|
{
|
||
|
|
var exists = await conn.ExecuteScalarAsync<int>("""
|
||
|
|
SELECT COUNT(*)
|
||
|
|
FROM sys.partition_functions pf
|
||
|
|
JOIN sys.partition_range_values prv ON prv.function_id = pf.function_id
|
||
|
|
WHERE pf.name = @Name AND CAST(prv.value AS DATETIME2(3)) = @Target;
|
||
|
|
""", new { Name = pf, Target = target });
|
||
|
|
|
||
|
|
if (exists == 0)
|
||
|
|
{
|
||
|
|
// Parameterized partition function name would require dynamic SQL; whitelisted above.
|
||
|
|
var sql = $"ALTER PARTITION FUNCTION {pf}() SPLIT RANGE (@Target);";
|
||
|
|
await conn.ExecuteAsync(sql, new { Target = target });
|
||
|
|
affected++;
|
||
|
|
_logger.LogInformation("Partition boundary {Boundary:yyyy-MM-dd} added to {Function}", target, pf);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_logger.LogInformation(
|
||
|
|
"AuditPartitionManagerJob completed — {Affected} partition function(s) extended (target: {Target:yyyy-MM-dd})",
|
||
|
|
affected, target);
|
||
|
|
}
|
||
|
|
}
|