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 = "默认 30 分钟一次")] public class S8WatchSchedulerJob : IJob { private readonly IServiceScopeFactory _scopeFactory; private readonly ISchedulerFactory _schedulerFactory; private readonly ILogger _logger; // G-08 调度周期(毫秒)。属性固化锚点,见 G08-02 配置分类表第 2 项。 // 既供 [Period] 使用,也供首次激活日志打印,避免两处数值漂移。 public const int IntervalMs = 1800000; // G-08 固定租户 / 工厂上下文。与 G-01 验收口径一致。 private const long DefaultTenantId = 1; private const long DefaultFactoryId = 1; // G-08 作业与触发器 ID。用于自动停调度时精确定位本作业。 private const string JobId = "job_s8_watch_scheduler"; // G08-05 运行期自动停调度:连续失败阈值(代码常量锚点,见 G08-02 配置分类表第 7 项)。 private const int FailurePauseThreshold = 2; // G08-02 首次作业激活可见性:作业首次激活时打印一次关键运行参数的生效值,后续周期不重复。 // 仅覆盖属性固化与代码常量两类;appsettings 与作业管理两类的当前值不在此打印。 private static int _firstActivationLogged; // G08-05 连续失败计数(仅同进程内有效;跨进程由 Admin.NET 作业管理手工介入,见 G08-03)。 private static int _consecutiveFailureCount; public S8WatchSchedulerJob( IServiceScopeFactory scopeFactory, ISchedulerFactory schedulerFactory, ILoggerFactory loggerFactory) { _scopeFactory = scopeFactory; _schedulerFactory = schedulerFactory; _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} Concurrent=false TenantId={TenantId} FactoryId={FactoryId}", IntervalMs, DefaultTenantId, DefaultFactoryId); } var triggeredAt = DateTime.Now; using var scope = _scopeFactory.CreateScope(); var scheduler = scope.ServiceProvider.GetRequiredService(); try { var results = await scheduler.CreateExceptionsAsync(DefaultTenantId, DefaultFactoryId); var created = results.Count(r => r.Created); var skipped = results.Count(r => r.Skipped); var failed = results.Count(r => !r.Created && !r.Skipped); var firstFailure = results.FirstOrDefault(r => !r.Created && !r.Skipped); _logger.LogInformation(BuildTraceLine( triggeredAt: triggeredAt, result: "success", hits: results.Count, created: created, skipped: skipped, failed: failed, failureSummary: TruncateSummary(firstFailure?.ErrorMessage ?? firstFailure?.Reason))); // 本轮主链未抛异常 → 重置连续失败计数(单条 create_failed 不计入调度失败)。 Interlocked.Exchange(ref _consecutiveFailureCount, 0); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation(BuildTraceLine( triggeredAt: triggeredAt, result: "cancelled", hits: 0, created: 0, skipped: 0, failed: 0, failureSummary: null)); // 取消不计入连续失败。 } catch (Exception ex) { _logger.LogError(ex, BuildTraceLine( triggeredAt: triggeredAt, result: "error", hits: 0, created: 0, skipped: 0, failed: 0, failureSummary: TruncateSummary(ex.Message))); var count = Interlocked.Increment(ref _consecutiveFailureCount); if (count >= FailurePauseThreshold) { TryAutoPause(count, ex); } } } // G08-05 运行期自动停调度:命中 FailurePauseThreshold 后调用框架原生暂停作业能力; // 不自建停调度通道;停调度原因单独留痕一行,便于检索。 private void TryAutoPause(int failureCount, Exception lastError) { try { var scheduleResult = _schedulerFactory.TryPauseJob(JobId, out _); _logger.LogWarning( "S8WatchSchedulerJob 自动停调度:FailureCount={FailureCount} Threshold={Threshold} ScheduleResult={ScheduleResult} Reason={Reason}", failureCount, FailurePauseThreshold, scheduleResult, TruncateSummary(lastError.Message)); } catch (Exception pauseEx) { // 停调度本身失败不重试、不回退;仅记录,等待人工介入。 _logger.LogError(pauseEx, "S8WatchSchedulerJob 自动停调度调用失败:FailureCount={FailureCount}", failureCount); } } // G08-04 结构化留痕:单行 JSON 字符串,承载最低 6 字段(触发时间 / 执行结果 / 命中数 / 创建数 / 跳过数 / 失败摘要)。 // 不建新表、不上日志库、不上链路追踪;失败摘要仅截断首条错误,不做异常归集。 private static string BuildTraceLine( DateTime triggeredAt, string result, int hits, int created, int skipped, int failed, string? failureSummary) { var payload = new { job = "S8WatchSchedulerJob", triggeredAt = triggeredAt.ToString("yyyy-MM-dd HH:mm:ss"), result, hits, created, skipped, failed, 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]; } }