MdpOutboxDeadLetterAlertJob.cs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. using Furion.Schedule;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. namespace Admin.NET.Plugin.AiDOP.Job;
  7. /// <summary>
  8. /// WP10 S4b / D12:Outbox 死信与积压告警(LogWarning + 站内 SysNotice)。
  9. /// 每 5 分钟扫描;同条件告警间隔不少于 30 分钟,避免刷屏。无钉钉。
  10. /// </summary>
  11. [JobDetail("job_mdp_outbox_dl_alert", Description = "MDP Outbox 死信/积压告警", GroupName = "default", Concurrent = false)]
  12. [PeriodSeconds(300, TriggerId = "trigger_mdp_outbox_dl_alert", Description = "每 5 分钟扫描 Outbox 告警", RunOnStart = false)]
  13. public class MdpOutboxDeadLetterAlertJob : IJob
  14. {
  15. private const long NoticeReceiverUserId = 1300000000101L;
  16. private const string NoticeReceiverUserName = "超级管理员";
  17. private static readonly TimeSpan AlertCooldown = TimeSpan.FromMinutes(30);
  18. private static readonly TimeSpan StalePendingThreshold = TimeSpan.FromMinutes(15);
  19. private static readonly object Gate = new();
  20. private static DateTime? _lastAlertUtc;
  21. private static DateTime? _lastScanUtc;
  22. private static long _lastSeenDeadMaxId;
  23. private readonly IServiceScopeFactory _scopeFactory;
  24. private readonly ILogger _logger;
  25. public MdpOutboxDeadLetterAlertJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory)
  26. {
  27. _scopeFactory = scopeFactory;
  28. _logger = loggerFactory.CreateLogger(nameof(MdpOutboxDeadLetterAlertJob));
  29. }
  30. public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
  31. {
  32. using var scope = _scopeFactory.CreateScope();
  33. var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
  34. var now = DateTime.Now;
  35. var scanSince = _lastScanUtc?.ToLocalTime() ?? now.AddMinutes(-5);
  36. var newDeadCount = await db.Queryable<MdpOutbox>()
  37. .CountAsync(x => x.Status == 2 && x.UpdateTime >= scanSince, stoppingToken);
  38. var maxDeadId = await db.Queryable<MdpOutbox>()
  39. .Where(x => x.Status == 2)
  40. .MaxAsync(x => (long?)x.Id, stoppingToken) ?? 0L;
  41. var newDeadById = maxDeadId > _lastSeenDeadMaxId
  42. ? await db.Queryable<MdpOutbox>()
  43. .CountAsync(x => x.Status == 2 && x.Id > _lastSeenDeadMaxId, stoppingToken)
  44. : 0;
  45. var hasNewDead = newDeadCount > 0 || newDeadById > 0;
  46. double oldestPendingMinutes = 0;
  47. var pending = await db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 0, stoppingToken);
  48. if (pending > 0)
  49. {
  50. var oldest = await db.Queryable<MdpOutbox>()
  51. .Where(x => x.Status == 0)
  52. .OrderBy(x => x.CreateTime)
  53. .Select(x => x.CreateTime)
  54. .FirstAsync(stoppingToken);
  55. oldestPendingMinutes = (now - oldest).TotalMinutes;
  56. }
  57. var stalePending = oldestPendingMinutes > StalePendingThreshold.TotalMinutes;
  58. _lastScanUtc = DateTime.UtcNow;
  59. if (maxDeadId > _lastSeenDeadMaxId)
  60. _lastSeenDeadMaxId = maxDeadId;
  61. if (!hasNewDead && !stalePending)
  62. return;
  63. lock (Gate)
  64. {
  65. if (_lastAlertUtc.HasValue && DateTime.UtcNow - _lastAlertUtc.Value < AlertCooldown)
  66. return;
  67. _lastAlertUtc = DateTime.UtcNow;
  68. }
  69. var deadTotal = await db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 2, stoppingToken);
  70. var msg =
  71. $"Outbox 告警:新增死信≈{Math.Max(newDeadCount, newDeadById)}(累计死信={deadTotal})," +
  72. $"待推={pending},最老待推≈{oldestPendingMinutes:F1} 分钟。" +
  73. $"请到「出站回写队列」查看 /aidop/data-platform/outbox。";
  74. _logger.LogWarning("[MdpOutboxDeadLetterAlertJob] {Message}", msg);
  75. try
  76. {
  77. // 后台作业无登录态,直接落 SysNotice + 一条 SysNoticeUser(超管),不走 SysNoticeService.InitNoticeInfo
  78. var notice = new SysNotice
  79. {
  80. Title = "Outbox死信/积压告警",
  81. Content = msg,
  82. Type = NoticeTypeEnum.NOTICE,
  83. PublicUserId = NoticeReceiverUserId,
  84. PublicUserName = NoticeReceiverUserName,
  85. PublicTime = now,
  86. Status = NoticeStatusEnum.PUBLIC,
  87. CreateTime = now,
  88. CreateUserId = NoticeReceiverUserId,
  89. CreateUserName = NoticeReceiverUserName,
  90. };
  91. var noticeId = await db.Insertable(notice).ExecuteReturnSnowflakeIdAsync(stoppingToken);
  92. await db.Insertable(new SysNoticeUser
  93. {
  94. NoticeId = noticeId,
  95. UserId = NoticeReceiverUserId,
  96. ReadStatus = NoticeUserStatusEnum.UNREAD,
  97. }).ExecuteCommandAsync(stoppingToken);
  98. }
  99. catch (Exception ex)
  100. {
  101. _logger.LogError(ex, "[MdpOutboxDeadLetterAlertJob] SysNotice 写入失败");
  102. }
  103. }
  104. }