S8LogRetentionCleanupJob.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Furion.Schedule;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. using SqlSugar;
  7. namespace Admin.NET.Plugin.AiDOP.Job;
  8. /// <summary>
  9. /// S8-LOG-RETENTION-CLEANUP-P1-2:S8 日志保留清理。
  10. /// 每日凌晨分批物理 DELETE 三张日志表过期数据:
  11. /// - ado_s8_detection_log:30 天保留(detected_at &lt; now - 30d)
  12. /// - ado_s8_notification_log:90 天保留(created_at &lt; now - 90d)
  13. /// - SysJobTriggerRecord:14 天保留(CreatedTime &lt; now - 14d)
  14. /// 默认 OFF;env override AIDOP_S8_LOG_CLEANUP_ENABLED=true 强制开启。
  15. /// 分批:单批 CleanupBatchSize 行;单表单次 Job 最多 MaxCleanupBatchesPerTable 批;
  16. /// 达到单次上限后剩余过期数据在下一日继续清理,避免单次 Job 长时间占用 DB。
  17. /// SqlSugar Deleteable 不支持 Take,采用 Queryable.Take(N).Select(Id) + Deleteable.Where(ids.Contains) 两步删除。
  18. /// 与 Admin.NET.Core/Job/LogJob.cs 同走 [Daily](00:00),两 Job 删除不同表无锁冲突。
  19. /// AdoS8DetectionLog.is_deleted 字段对 Queryable 不做默认过滤,本批一并物理删除软删行与活跃过期行。
  20. /// </summary>
  21. [JobDetail("job_s8_log_retention_cleanup", Description = "S8 日志保留清理",
  22. GroupName = "default", Concurrent = false)]
  23. [Daily(TriggerId = "trigger_s8_log_retention_cleanup", Description = "每日凌晨执行")]
  24. public class S8LogRetentionCleanupJob : IJob
  25. {
  26. private readonly IServiceScopeFactory _scopeFactory;
  27. private readonly ILogger _logger;
  28. // 禁用日志单进程内只输出一次,避免每次触发都刷日志。
  29. private static int _firstDisabledLogged;
  30. // env override 命名沿用 P0 批次的 AIDOP_* 前缀(与 S8WatchSchedulerJob 等一致)。
  31. private const string EnvCleanupEnabled = "AIDOP_S8_LOG_CLEANUP_ENABLED";
  32. // 已拍板分级保留天数。
  33. private const int DetectionLogRetentionDays = 30;
  34. private const int NotificationLogRetentionDays = 90;
  35. private const int JobTriggerRecordRetentionDays = 14;
  36. // 分批参数:单批 1000 行;单表单次 Job 最多 50 批(5w 行/表/日上限),残余下一日继续。
  37. private const int CleanupBatchSize = 1000;
  38. private const int MaxCleanupBatchesPerTable = 50;
  39. public S8LogRetentionCleanupJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
  40. {
  41. _scopeFactory = scopeFactory;
  42. _logger = loggerFactory.CreateLogger(nameof(S8LogRetentionCleanupJob));
  43. }
  44. public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
  45. {
  46. if (!AidopJobGate.ShouldRun(nameof(S8LogRetentionCleanupJob), _logger)) return;
  47. // 默认 OFF;env override AIDOP_S8_LOG_CLEANUP_ENABLED=true 强制开启。
  48. // 与 P0 Scheduler:Enabled / S8:Scheduler:Enabled 独立,本批不复用既有 gate。
  49. var envRaw = Environment.GetEnvironmentVariable(EnvCleanupEnabled);
  50. if (!bool.TryParse(envRaw, out var enabled) || !enabled)
  51. {
  52. if (Interlocked.Exchange(ref _firstDisabledLogged, 1) == 0)
  53. {
  54. _logger.LogInformation(
  55. "S8LogRetentionCleanupJob 被配置禁用:{EnvName} 未设置或非 true",
  56. EnvCleanupEnabled);
  57. }
  58. return;
  59. }
  60. using var scope = _scopeFactory.CreateScope();
  61. var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>().CopyNew();
  62. var totalDetection = 0;
  63. var totalNotification = 0;
  64. var totalTrigger = 0;
  65. try
  66. {
  67. totalDetection = await CleanupDetectionLogAsync(db, stoppingToken);
  68. }
  69. catch (Exception ex)
  70. {
  71. _logger.LogError(ex, "ado_s8_detection_log 清理失败");
  72. }
  73. try
  74. {
  75. totalNotification = await CleanupNotificationLogAsync(db, stoppingToken);
  76. }
  77. catch (Exception ex)
  78. {
  79. _logger.LogError(ex, "ado_s8_notification_log 清理失败");
  80. }
  81. try
  82. {
  83. totalTrigger = await CleanupJobTriggerRecordAsync(db, stoppingToken);
  84. }
  85. catch (Exception ex)
  86. {
  87. _logger.LogError(ex, "SysJobTriggerRecord 清理失败");
  88. }
  89. _logger.LogInformation(
  90. "S8LogRetentionCleanupJob 本轮删除摘要:detection_log={DetectionCount} notification_log={NotificationCount} sys_job_trigger_record={TriggerCount}",
  91. totalDetection, totalNotification, totalTrigger);
  92. }
  93. private async Task<int> CleanupDetectionLogAsync(ISqlSugarClient db, CancellationToken token)
  94. {
  95. var cutoff = DateTime.Now.AddDays(-DetectionLogRetentionDays);
  96. var total = 0;
  97. for (var i = 0; i < MaxCleanupBatchesPerTable; i++)
  98. {
  99. if (token.IsCancellationRequested) break;
  100. var ids = await db.Queryable<AdoS8DetectionLog>()
  101. .Where(x => x.DetectedAt < cutoff)
  102. .Take(CleanupBatchSize)
  103. .Select(x => x.Id)
  104. .ToListAsync();
  105. if (ids.Count == 0) break;
  106. var affected = await db.Deleteable<AdoS8DetectionLog>()
  107. .Where(x => ids.Contains(x.Id))
  108. .ExecuteCommandAsync(token);
  109. total += affected;
  110. if (ids.Count < CleanupBatchSize) break;
  111. }
  112. return total;
  113. }
  114. private async Task<int> CleanupNotificationLogAsync(ISqlSugarClient db, CancellationToken token)
  115. {
  116. var cutoff = DateTime.Now.AddDays(-NotificationLogRetentionDays);
  117. var total = 0;
  118. for (var i = 0; i < MaxCleanupBatchesPerTable; i++)
  119. {
  120. if (token.IsCancellationRequested) break;
  121. var ids = await db.Queryable<AdoS8NotificationLog>()
  122. .Where(x => x.CreatedAt < cutoff)
  123. .Take(CleanupBatchSize)
  124. .Select(x => x.Id)
  125. .ToListAsync();
  126. if (ids.Count == 0) break;
  127. var affected = await db.Deleteable<AdoS8NotificationLog>()
  128. .Where(x => ids.Contains(x.Id))
  129. .ExecuteCommandAsync(token);
  130. total += affected;
  131. if (ids.Count < CleanupBatchSize) break;
  132. }
  133. return total;
  134. }
  135. private async Task<int> CleanupJobTriggerRecordAsync(ISqlSugarClient db, CancellationToken token)
  136. {
  137. var cutoff = DateTime.Now.AddDays(-JobTriggerRecordRetentionDays);
  138. var total = 0;
  139. for (var i = 0; i < MaxCleanupBatchesPerTable; i++)
  140. {
  141. if (token.IsCancellationRequested) break;
  142. var ids = await db.Queryable<SysJobTriggerRecord>()
  143. .Where(x => x.CreatedTime != null && x.CreatedTime < cutoff)
  144. .Take(CleanupBatchSize)
  145. .Select(x => x.Id)
  146. .ToListAsync();
  147. if (ids.Count == 0) break;
  148. var affected = await db.Deleteable<SysJobTriggerRecord>()
  149. .Where(x => ids.Contains(x.Id))
  150. .ExecuteCommandAsync(token);
  151. total += affected;
  152. if (ids.Count < CleanupBatchSize) break;
  153. }
  154. return total;
  155. }
  156. }