| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- 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;
- /// <summary>
- /// G-08:S8 自动监控主链定时调度作业。
- /// 固定租户 / 工厂上下文下,按周期触发一次 G-01 主链(<see cref="S8WatchSchedulerService.CreateExceptionsAsync"/>)。
- /// 本轮仅完成"正式定时触发接入";不在调度层追加业务逻辑、不改写主链入参语义、不做真实 SQL / 认证链路 / SLA 治理。
- /// 周期由 [Period] 属性固化为默认值(30 分钟);运行期调整走 Admin.NET 作业管理,不走配置中心。
- /// 开关:默认通过 Admin.NET 作业管理的触发器状态控制;本作业构造不自动启动。
- /// </summary>
- [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<S8WatchSchedulerService>();
- 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];
- }
- }
|