Files
SIG-CM2.0/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditPartitionManagerJob.cs

65 lines
2.6 KiB
C#
Raw Normal View History

feat(jobs): 3 audit maintenance jobs (Quartz.NET, UDT-010 B11) Agrega Quartz.Extensions.Hosting 3.13.1 al catálogo central. SIGCM2.Infrastructure/Audit/Jobs/: - AuditPartitionManagerJob — mensual (cron '0 0 2 1 * ?', UTC). Extiende pf_AuditEvent_Monthly y pf_SecurityEvent_Monthly con SPLIT RANGE para el mes+2 (mantiene +1 de buffer). Idempotente: verifica existencia antes. - AuditRetentionEnforcerJob — anual (cron '0 0 3 1 1 ?', UTC). DELETE rows > 10 años en AuditEvent y > 5 años en SecurityEvent. Temporal history se purga solo vía HISTORY_RETENTION_PERIOD del engine. - AuditIntegrityCheckJob — semanal domingos (cron '0 0 1 ? * SUN', UTC). Valida SYSTEM_VERSIONING=ON + partitions próximos 3 meses. Emite SecurityEvent 'system.integrity_alert' failure via ISecurityEventLogger cuando detecta inconsistencias. AuditMaintenanceRegistration.cs: - services.AddAuditMaintenance(configuration) wraps AddQuartz + AddQuartzHostedService con los 3 triggers crónicos. Program.cs: - builder.Services.AddAuditMaintenance(configuration) wired ONLY en entornos productivos — skipeado en 'Testing' para que los integration tests no disparen los triggers cron durante el ciclo de vida del TestWebAppFactory. Row-based DELETE en RetentionEnforcerJob es la opción conservadora para la primera generación — cuando los volúmenes lo justifiquen (>200M filas), se upgradea a SWITCH OUT + DROP para partition-level drop. Documentado en comentario de la clase. Tests (Strict TDD, integration): - AuditJobsTests (3): PartitionManager crea target boundary + idempotencia, RetentionEnforcer purga > threshold (10y audit, 5y security), IntegrityCheck all-OK no emite alert. Suite: 381/381 Application.Tests + 147/147 Api.Tests = 528/528 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-6 #REQ-SEC-5, design, tasks#B11}
2026-04-16 17:10:43 -03:00
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);
}
}