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];
}
}