S8WatchSchedulerJob.cs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. using Admin.NET.Plugin.AiDOP.Service.S8;
  2. using Furion.Schedule;
  3. using Microsoft.Extensions.DependencyInjection;
  4. using Microsoft.Extensions.Logging;
  5. using System.Text.Json;
  6. namespace Admin.NET.Plugin.AiDOP.Job;
  7. /// <summary>
  8. /// G-08:S8 自动监控主链定时调度作业。
  9. /// 固定租户 / 工厂上下文下,按周期触发一次 G-01 主链(<see cref="S8WatchSchedulerService.CreateExceptionsAsync"/>)。
  10. /// 本轮仅完成"正式定时触发接入";不在调度层追加业务逻辑、不改写主链入参语义、不做真实 SQL / 认证链路 / SLA 治理。
  11. /// 周期由 [Period] 属性固化为默认值(30 分钟);运行期调整走 Admin.NET 作业管理,不走配置中心。
  12. /// 开关:默认通过 Admin.NET 作业管理的触发器状态控制;本作业构造不自动启动。
  13. /// </summary>
  14. [JobDetail("job_s8_watch_scheduler", Description = "S8 自动监控主链定时调度",
  15. GroupName = "default", Concurrent = false)]
  16. [Period(S8WatchSchedulerJob.IntervalMs, TriggerId = "trigger_s8_watch_scheduler", Description = "S8-SCHED-EXEC-1:DB 驱动派发,1 分钟节拍")]
  17. public class S8WatchSchedulerJob : IJob
  18. {
  19. private readonly IServiceScopeFactory _scopeFactory;
  20. private readonly ILogger _logger;
  21. // S8-SCHED-EXEC-1:节拍 5min → 1min;单规则节奏由 watch_rule.poll_interval_seconds + next_run_at 控制,
  22. // Job 仅做 tick;未到期规则在 PickReadyRulesAsync 阶段自然过滤。
  23. public const int IntervalMs = 60000;
  24. // 默认租户 / 工厂上下文。与 G-01 / S8-SCHED-SCHEMA-1 验收口径一致。
  25. private const long DefaultTenantId = 1;
  26. private const long DefaultFactoryId = 1;
  27. // 单 tick 最多抢锁规则数;规模上来后可下放到 appsettings。当前 dev 仅 3 条 demo,给 32 留余量。
  28. private const int BatchSize = 32;
  29. // 首次激活打印锚点。
  30. private static int _firstActivationLogged;
  31. public S8WatchSchedulerJob(
  32. IServiceScopeFactory scopeFactory,
  33. ILoggerFactory loggerFactory)
  34. {
  35. _scopeFactory = scopeFactory;
  36. _logger = loggerFactory.CreateLogger(nameof(S8WatchSchedulerJob));
  37. }
  38. public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
  39. {
  40. if (Interlocked.Exchange(ref _firstActivationLogged, 1) == 0)
  41. {
  42. _logger.LogInformation(
  43. "S8WatchSchedulerJob 首次激活:IntervalMs={IntervalMs} BatchSize={Batch} TenantId={Tenant} FactoryId={Factory} LockedBy={LockedBy}",
  44. IntervalMs, BatchSize, DefaultTenantId, DefaultFactoryId, BuildLockedBy());
  45. }
  46. var triggeredAt = DateTime.Now;
  47. using var scope = _scopeFactory.CreateScope();
  48. var scheduler = scope.ServiceProvider.GetRequiredService<S8WatchSchedulerService>();
  49. try
  50. {
  51. var lockedBy = BuildLockedBy();
  52. var summary = await scheduler.RunDispatchTickAsync(DefaultTenantId, DefaultFactoryId, BatchSize, lockedBy);
  53. _logger.LogInformation(BuildTraceLine(triggeredAt, "success", summary, null));
  54. }
  55. catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
  56. {
  57. _logger.LogInformation(BuildTraceLine(triggeredAt, "cancelled", null, null));
  58. }
  59. catch (Exception ex)
  60. {
  61. // RunDispatchTickAsync 内部已吞所有单规则异常;此处仅兜底。
  62. _logger.LogError(ex, BuildTraceLine(triggeredAt, "error", null, TruncateSummary(ex.Message)));
  63. }
  64. }
  65. private static string BuildLockedBy()
  66. {
  67. try
  68. {
  69. var pid = Environment.ProcessId;
  70. return $"{Environment.MachineName}-{pid}";
  71. }
  72. catch
  73. {
  74. return "unknown-host";
  75. }
  76. }
  77. // 单行 JSON 留痕:tick 维度 6 字段(触发时间 / 执行结果 / picked / created / refreshed / pending / failed / leaseReleased / runId)。
  78. private static string BuildTraceLine(
  79. DateTime triggeredAt, string result, S8DispatchTickResult? summary, string? failureSummary)
  80. {
  81. var payload = new
  82. {
  83. job = "S8WatchSchedulerJob",
  84. triggeredAt = triggeredAt.ToString("yyyy-MM-dd HH:mm:ss"),
  85. result,
  86. tickId = summary?.TickId,
  87. runId = summary?.RunId,
  88. picked = summary?.Picked ?? 0,
  89. success = summary?.Success ?? 0,
  90. ruleFailed = summary?.Failed ?? 0,
  91. created = summary?.Created ?? 0,
  92. refreshed = summary?.Refreshed ?? 0,
  93. pending = summary?.Pending ?? 0,
  94. perRuleFailed = summary?.PerRuleFailed ?? 0,
  95. leaseReleased = summary?.LeaseReleased ?? 0,
  96. failureSummary
  97. };
  98. return "S8WatchSchedulerJob 留痕 " + JsonSerializer.Serialize(payload);
  99. }
  100. private static string? TruncateSummary(string? raw)
  101. {
  102. if (string.IsNullOrWhiteSpace(raw)) return null;
  103. var oneLine = raw.Replace('\n', ' ').Replace('\r', ' ').Trim();
  104. return oneLine.Length <= 200 ? oneLine : oneLine[..200];
  105. }
  106. }