using Admin.NET.Plugin.AiDOP.Service.S8; using Furion.Schedule; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Text.Json; namespace Admin.NET.Plugin.AiDOP.Job; /// /// G-08:S8 自动监控主链定时调度作业。 /// 固定租户 / 工厂上下文下,按周期触发一次 G-01 主链()。 /// 本轮仅完成"正式定时触发接入";不在调度层追加业务逻辑、不改写主链入参语义、不做真实 SQL / 认证链路 / SLA 治理。 /// 周期由 [Period] 属性固化为默认值(30 分钟);运行期调整走 Admin.NET 作业管理,不走配置中心。 /// 开关:默认通过 Admin.NET 作业管理的触发器状态控制;本作业构造不自动启动。 /// [JobDetail("job_s8_watch_scheduler", Description = "S8 自动监控主链定时调度", GroupName = "default", Concurrent = false)] [Period(S8WatchSchedulerJob.IntervalMs, TriggerId = "trigger_s8_watch_scheduler", Description = "S8-SCHED-EXEC-1:DB 驱动派发,1 分钟节拍")] public class S8WatchSchedulerJob : IJob { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; // S8-SCHED-EXEC-1:节拍 5min → 1min;单规则节奏由 watch_rule.poll_interval_seconds + next_run_at 控制, // Job 仅做 tick;未到期规则在 PickReadyRulesAsync 阶段自然过滤。 public const int IntervalMs = 60000; // 默认租户 / 工厂上下文。与 G-01 / S8-SCHED-SCHEMA-1 验收口径一致。 private const long DefaultTenantId = 1; private const long DefaultFactoryId = 1; // 单 tick 最多抢锁规则数;规模上来后可下放到 appsettings。当前 dev 仅 3 条 demo,给 32 留余量。 private const int BatchSize = 32; // 首次激活打印锚点。 private static int _firstActivationLogged; public S8WatchSchedulerJob( IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory) { _scopeFactory = scopeFactory; _logger = loggerFactory.CreateLogger(nameof(S8WatchSchedulerJob)); } public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) { if (Interlocked.Exchange(ref _firstActivationLogged, 1) == 0) { _logger.LogInformation( "S8WatchSchedulerJob 首次激活:IntervalMs={IntervalMs} BatchSize={Batch} TenantId={Tenant} FactoryId={Factory} LockedBy={LockedBy}", IntervalMs, BatchSize, DefaultTenantId, DefaultFactoryId, BuildLockedBy()); } var triggeredAt = DateTime.Now; using var scope = _scopeFactory.CreateScope(); var scheduler = scope.ServiceProvider.GetRequiredService(); try { var lockedBy = BuildLockedBy(); var summary = await scheduler.RunDispatchTickAsync(DefaultTenantId, DefaultFactoryId, BatchSize, lockedBy); _logger.LogInformation(BuildTraceLine(triggeredAt, "success", summary, null)); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation(BuildTraceLine(triggeredAt, "cancelled", null, null)); } catch (Exception ex) { // RunDispatchTickAsync 内部已吞所有单规则异常;此处仅兜底。 _logger.LogError(ex, BuildTraceLine(triggeredAt, "error", null, TruncateSummary(ex.Message))); } } private static string BuildLockedBy() { try { var pid = Environment.ProcessId; return $"{Environment.MachineName}-{pid}"; } catch { return "unknown-host"; } } // 单行 JSON 留痕:tick 维度 6 字段(触发时间 / 执行结果 / picked / created / refreshed / pending / failed / leaseReleased / runId)。 private static string BuildTraceLine( DateTime triggeredAt, string result, S8DispatchTickResult? summary, string? failureSummary) { var payload = new { job = "S8WatchSchedulerJob", triggeredAt = triggeredAt.ToString("yyyy-MM-dd HH:mm:ss"), result, tickId = summary?.TickId, runId = summary?.RunId, picked = summary?.Picked ?? 0, success = summary?.Success ?? 0, ruleFailed = summary?.Failed ?? 0, created = summary?.Created ?? 0, refreshed = summary?.Refreshed ?? 0, pending = summary?.Pending ?? 0, perRuleFailed = summary?.PerRuleFailed ?? 0, leaseReleased = summary?.LeaseReleased ?? 0, failureSummary }; return "S8WatchSchedulerJob 留痕 " + JsonSerializer.Serialize(payload); } private static string? TruncateSummary(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return null; var oneLine = raw.Replace('\n', ' ').Replace('\r', ' ').Trim(); return oneLine.Length <= 200 ? oneLine : oneLine[..200]; } }