|
@@ -1,5 +1,6 @@
|
|
|
using Admin.NET.Plugin.AiDOP.Service.S8;
|
|
using Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
using Furion.Schedule;
|
|
using Furion.Schedule;
|
|
|
|
|
+using Microsoft.Extensions.Configuration;
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging;
|
|
|
using System.Text.Json;
|
|
using System.Text.Json;
|
|
@@ -19,12 +20,19 @@ namespace Admin.NET.Plugin.AiDOP.Job;
|
|
|
public class S8WatchSchedulerJob : IJob
|
|
public class S8WatchSchedulerJob : IJob
|
|
|
{
|
|
{
|
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
|
|
|
+ private readonly IConfiguration _configuration;
|
|
|
private readonly ILogger _logger;
|
|
private readonly ILogger _logger;
|
|
|
|
|
|
|
|
// S8-SCHED-EXEC-1:节拍 5min → 1min;单规则节奏由 watch_rule.poll_interval_seconds + next_run_at 控制,
|
|
// S8-SCHED-EXEC-1:节拍 5min → 1min;单规则节奏由 watch_rule.poll_interval_seconds + next_run_at 控制,
|
|
|
// Job 仅做 tick;未到期规则在 PickReadyRulesAsync 阶段自然过滤。
|
|
// Job 仅做 tick;未到期规则在 PickReadyRulesAsync 阶段自然过滤。
|
|
|
public const int IntervalMs = 60000;
|
|
public const int IntervalMs = 60000;
|
|
|
|
|
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:业务执行节拍配置项与下限。
|
|
|
|
|
+ // 业务节拍由 appsettings 的 S8:Scheduler:WatchTickIntervalMs 承载,默认 300000 ms (5 分钟);
|
|
|
|
|
+ // 硬件唤醒拍由 [Period(60000, ...)] 注解承载;下限 60000 ms 与硬件拍对齐,配置小于下限按下限处理。
|
|
|
|
|
+ private const int DefaultWatchTickIntervalMs = 300000;
|
|
|
|
|
+ private const int MinWatchTickIntervalMs = 60000;
|
|
|
|
|
+
|
|
|
// 默认租户 / 工厂上下文。与 G-01 / S8-SCHED-SCHEMA-1 验收口径一致。
|
|
// 默认租户 / 工厂上下文。与 G-01 / S8-SCHED-SCHEMA-1 验收口径一致。
|
|
|
private const long DefaultTenantId = 1;
|
|
private const long DefaultTenantId = 1;
|
|
|
private const long DefaultFactoryId = 1;
|
|
private const long DefaultFactoryId = 1;
|
|
@@ -35,21 +43,106 @@ public class S8WatchSchedulerJob : IJob
|
|
|
// 首次激活打印锚点。
|
|
// 首次激活打印锚点。
|
|
|
private static int _firstActivationLogged;
|
|
private static int _firstActivationLogged;
|
|
|
|
|
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:禁用提示锚点 + 业务节拍 due-skip 锚点。
|
|
|
|
|
+ // _firstDisabledLogged 控制禁用日志单进程内只输出一次,避免每 tick 刷日志。
|
|
|
|
|
+ // _lastBusinessTickUtcTicks 记录上一次进入业务体(RunDispatchTickAsync)时的 UTC ticks,
|
|
|
|
|
+ // 用于"硬件拍 60s + 业务拍 5min"模式下的 due-skip 判定。
|
|
|
|
|
+ // _firstParseFailLogged 控制 AIDOP_* env 解析失败 warn 单进程内只输出一次(首个失败 env 名)。
|
|
|
|
|
+ private static int _firstDisabledLogged;
|
|
|
|
|
+ private static long _lastBusinessTickUtcTicks;
|
|
|
|
|
+ private static int _firstParseFailLogged;
|
|
|
|
|
+
|
|
|
|
|
+ // S8-SCHEDULER-CONFIG-ORDER-FIX(合入 P0):AIDOP_* env override 名称。
|
|
|
|
|
+ // Furion 4.9.8.24 把 Configuration/*.{Environment}.json 追加在 ASP.NET Core
|
|
|
|
|
+ // EnvironmentVariablesConfigurationProvider 之后,JSON 覆盖了 env,故此处用
|
|
|
|
|
+ // Environment.GetEnvironmentVariable 做 S8 调度开关专项 override;
|
|
|
|
|
+ // 命名前缀 AIDOP_ 与 SqlSugarSetup.cs 既有约定(AIDOP_DB_WAIT_MAX_SECONDS 等)一致。
|
|
|
|
|
+ private const string EnvSchedulerEnabled = "AIDOP_SCHEDULER_ENABLED";
|
|
|
|
|
+ private const string EnvS8SchedulerEnabled = "AIDOP_S8_SCHEDULER_ENABLED";
|
|
|
|
|
+ private const string EnvS8WatchTickMs = "AIDOP_S8_SCHEDULER_WATCH_TICK_MS";
|
|
|
|
|
+
|
|
|
public S8WatchSchedulerJob(
|
|
public S8WatchSchedulerJob(
|
|
|
IServiceScopeFactory scopeFactory,
|
|
IServiceScopeFactory scopeFactory,
|
|
|
|
|
+ IConfiguration configuration,
|
|
|
ILoggerFactory loggerFactory)
|
|
ILoggerFactory loggerFactory)
|
|
|
{
|
|
{
|
|
|
_scopeFactory = scopeFactory;
|
|
_scopeFactory = scopeFactory;
|
|
|
|
|
+ _configuration = configuration;
|
|
|
_logger = loggerFactory.CreateLogger(nameof(S8WatchSchedulerJob));
|
|
_logger = loggerFactory.CreateLogger(nameof(S8WatchSchedulerJob));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
|
|
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
|
|
|
{
|
|
{
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:环境级 + S8 业务级双开关 gate。
|
|
|
|
|
+ // 任一为 false 时直接早退;早退前不访问任何业务 service / DB。
|
|
|
|
|
+ // env override(合入 CONFIG-ORDER-FIX):env 不存在/空白 → 沿用 JSON;
|
|
|
|
|
+ // 存在但 bool.TryParse 失败 → 保留 JSON 值 + 单次 warn(首个失败 env 名,不输出 value)。
|
|
|
|
|
+ var schedulerEnabled = _configuration.GetValue("Scheduler:Enabled", true);
|
|
|
|
|
+ var s8SchedulerEnabled = _configuration.GetValue("S8:Scheduler:Enabled", false);
|
|
|
|
|
+ string? parseFailEnvName = null;
|
|
|
|
|
+ var envSchedulerRaw = Environment.GetEnvironmentVariable(EnvSchedulerEnabled);
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(envSchedulerRaw))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (bool.TryParse(envSchedulerRaw, out var v)) schedulerEnabled = v;
|
|
|
|
|
+ else parseFailEnvName ??= EnvSchedulerEnabled;
|
|
|
|
|
+ }
|
|
|
|
|
+ var envS8SchedulerRaw = Environment.GetEnvironmentVariable(EnvS8SchedulerEnabled);
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(envS8SchedulerRaw))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (bool.TryParse(envS8SchedulerRaw, out var v)) s8SchedulerEnabled = v;
|
|
|
|
|
+ else parseFailEnvName ??= EnvS8SchedulerEnabled;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (parseFailEnvName != null && Interlocked.Exchange(ref _firstParseFailLogged, 1) == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning(
|
|
|
|
|
+ "S8WatchSchedulerJob env override 解析失败:{EnvName},已沿用配置值",
|
|
|
|
|
+ parseFailEnvName);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!schedulerEnabled || !s8SchedulerEnabled)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (Interlocked.Exchange(ref _firstDisabledLogged, 1) == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogInformation(
|
|
|
|
|
+ "S8WatchSchedulerJob 被配置禁用:Scheduler:Enabled={Scheduler} S8:Scheduler:Enabled={S8Scheduler}",
|
|
|
|
|
+ schedulerEnabled, s8SchedulerEnabled);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:业务节拍 due-skip。
|
|
|
|
|
+ // 硬件唤醒拍仍由 [Period(60000, ...)] 维持,但业务体只在配置的 WatchTickIntervalMs 周期上执行。
|
|
|
|
|
+ // 首次启用时 _lastBusinessTickUtcTicks==0,立即放行;后续 tick 不足 interval 时早退。
|
|
|
|
|
+ // 早退前不调用 S8WatchSchedulerService、不查询/写入数据库。
|
|
|
|
|
+ // env override(合入 CONFIG-ORDER-FIX):env 存在且 int.TryParse 成功 → 覆盖 JSON;
|
|
|
|
|
+ // 解析失败 → 保留 JSON 值 + 单次 warn(与 bool 解析失败共用 _firstParseFailLogged 锚点)。
|
|
|
|
|
+ // clamp 规则与 P0 一致:小于 MinWatchTickIntervalMs(60000)时强制按下限处理。
|
|
|
|
|
+ var tickIntervalMs = _configuration.GetValue("S8:Scheduler:WatchTickIntervalMs", DefaultWatchTickIntervalMs);
|
|
|
|
|
+ var envTickRaw = Environment.GetEnvironmentVariable(EnvS8WatchTickMs);
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(envTickRaw))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (int.TryParse(envTickRaw, out var v)) tickIntervalMs = v;
|
|
|
|
|
+ else if (Interlocked.Exchange(ref _firstParseFailLogged, 1) == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning(
|
|
|
|
|
+ "S8WatchSchedulerJob env override 解析失败:{EnvName},已沿用配置值",
|
|
|
|
|
+ EnvS8WatchTickMs);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tickIntervalMs < MinWatchTickIntervalMs) tickIntervalMs = MinWatchTickIntervalMs;
|
|
|
|
|
+ var nowUtcTicks = DateTime.UtcNow.Ticks;
|
|
|
|
|
+ var lastTicks = Interlocked.Read(ref _lastBusinessTickUtcTicks);
|
|
|
|
|
+ if (lastTicks != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ var elapsedMs = (nowUtcTicks - lastTicks) / TimeSpan.TicksPerMillisecond;
|
|
|
|
|
+ if (elapsedMs < tickIntervalMs) return;
|
|
|
|
|
+ }
|
|
|
|
|
+ Interlocked.Exchange(ref _lastBusinessTickUtcTicks, nowUtcTicks);
|
|
|
|
|
+
|
|
|
if (Interlocked.Exchange(ref _firstActivationLogged, 1) == 0)
|
|
if (Interlocked.Exchange(ref _firstActivationLogged, 1) == 0)
|
|
|
{
|
|
{
|
|
|
_logger.LogInformation(
|
|
_logger.LogInformation(
|
|
|
- "S8WatchSchedulerJob 首次激活:IntervalMs={IntervalMs} BatchSize={Batch} TenantId={Tenant} FactoryId={Factory} LockedBy={LockedBy}",
|
|
|
|
|
- IntervalMs, BatchSize, DefaultTenantId, DefaultFactoryId, BuildLockedBy());
|
|
|
|
|
|
|
+ "S8WatchSchedulerJob 首次激活:IntervalMs={IntervalMs} TickIntervalMs={TickInterval} BatchSize={Batch} TenantId={Tenant} FactoryId={Factory} LockedBy={LockedBy}",
|
|
|
|
|
+ IntervalMs, tickIntervalMs, BatchSize, DefaultTenantId, DefaultFactoryId, BuildLockedBy());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var triggeredAt = DateTime.Now;
|
|
var triggeredAt = DateTime.Now;
|
|
@@ -60,7 +153,12 @@ public class S8WatchSchedulerJob : IJob
|
|
|
{
|
|
{
|
|
|
var lockedBy = BuildLockedBy();
|
|
var lockedBy = BuildLockedBy();
|
|
|
var summary = await scheduler.RunDispatchTickAsync(DefaultTenantId, DefaultFactoryId, BatchSize, lockedBy);
|
|
var summary = await scheduler.RunDispatchTickAsync(DefaultTenantId, DefaultFactoryId, BatchSize, lockedBy);
|
|
|
- _logger.LogInformation(BuildTraceLine(triggeredAt, "success", summary, null));
|
|
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:完全空跑(6 项摘要全 0)降为 LogDebug;
|
|
|
|
|
+ // 任何非零字段(picked/failed/perRuleFailed/created/refreshed/leaseReleased)保持 LogInformation。
|
|
|
|
|
+ if (IsEmptyTick(summary))
|
|
|
|
|
+ _logger.LogDebug(BuildTraceLine(triggeredAt, "success", summary, null));
|
|
|
|
|
+ else
|
|
|
|
|
+ _logger.LogInformation(BuildTraceLine(triggeredAt, "success", summary, null));
|
|
|
}
|
|
}
|
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
|
|
{
|
|
{
|
|
@@ -116,4 +214,17 @@ public class S8WatchSchedulerJob : IJob
|
|
|
var oneLine = raw.Replace('\n', ' ').Replace('\r', ' ').Trim();
|
|
var oneLine = raw.Replace('\n', ' ').Replace('\r', ' ').Trim();
|
|
|
return oneLine.Length <= 200 ? oneLine : oneLine[..200];
|
|
return oneLine.Length <= 200 ? oneLine : oneLine[..200];
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // S8-SCHEDULER-P0-BLEEDING-STOP-CONFIG-1:完全空跑判定。
|
|
|
|
|
+ // 6 项摘要全 0 → 视为本 tick 无任何业务变化,日志降级到 Debug。
|
|
|
|
|
+ private static bool IsEmptyTick(S8DispatchTickResult? summary)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (summary == null) return true;
|
|
|
|
|
+ return summary.Picked == 0
|
|
|
|
|
+ && summary.Failed == 0
|
|
|
|
|
+ && summary.PerRuleFailed == 0
|
|
|
|
|
+ && summary.Created == 0
|
|
|
|
|
+ && summary.Refreshed == 0
|
|
|
|
|
+ && summary.LeaseReleased == 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|