using Microsoft.Extensions.Configuration;
namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
/// S8 后台自动任务标识。每个 Job 一个独立开关节。
public enum S8BackgroundJob
{
/// S8 自动监控主链调度(S8:WatchScheduler:Enabled)。
WatchScheduler,
/// SLA 超时自动升级(S8:TimeoutAutoEscalation:Enabled)。
TimeoutAutoEscalation,
/// ActiveFlow 卡死扫描(S8:ActiveFlowStuckScan:Enabled)。
ActiveFlowStuckScan,
}
///
/// S8-P0-2-PER-JOB-KILL-SWITCH-1:S8 后台任务三级开关求值。
///
/// 为什么需要:修复前三个 Job(WatchScheduler / TimeoutAutoEscalation / ActiveFlowStuckScan)
/// 读的是**完全相同**的两个键 Scheduler:Enabled + S8:Scheduler:Enabled,
/// 没有任何 per-job 键。于是「只想跑 Watch」在配置层不可表达 —— 2026-09-02 的受控启动里,
/// 打开总开关同时放行了另外两个 Job,其中 ActiveFlowStuckScan 因带 RunOnStart = true
/// 在重启瞬间就写入了 499 行告警日志。当时只能靠「先暂停 trigger 再重启」隔离,
/// 而该手段实测**不可靠**(两次重启结果相反:一次暂停态丢失、一次存活)。
///
/// 生效语义(三级与,任一 false 即停):
///
/// Effective = Scheduler:Enabled // 环境级 master kill switch,默认 true
/// && S8:Scheduler:Enabled // S8 业务级 master kill switch,默认 false
/// && S8:{Job}:Enabled // 单 Job 开关,默认 false
///
///
/// 默认 false 是刻意的:新增 per-job 键后,任何未显式声明该键的部署都保持不执行,
/// 宁可「本该跑却没跑」也不要「本不该跑却跑了」——本模块的事故类型全部属于后者。
///
/// env override:Furion 4.9.8.24 把 Configuration/*.json 装配在 ASP.NET Core
/// EnvironmentVariables provider **之后**,JSON 会盖掉 env,故所有开关都改用
/// 直读,命名沿用既有 AIDOP_ 前缀约定。
/// env 不存在 / 空白 → 沿用 JSON;存在但 bool.TryParse 失败 → 保留 JSON 值并回报
/// (由调用方做单次 warn,不输出 value)。
///
/// 与 RunOnStart 的关系:本类只做纯计算、不碰 DB / service。调用方必须在
/// ExecuteAsync 的**第一步**求值并在 false 时 return,因此即便
/// [Period(..., RunOnStart = true)] 在启动瞬间触发,也会在进入任何业务扫描 / 写入之前退出。
/// 不再需要、也不得依赖 trigger 暂停作为隔离手段。
///
public static class S8JobSwitch
{
/// 环境级总开关配置键。默认 true(保持既有部署行为)。
public const string EnvironmentEnabledKey = "Scheduler:Enabled";
/// S8 业务级总开关配置键。默认 false。
public const string MasterEnabledKey = "S8:Scheduler:Enabled";
public const string EnvEnvironmentEnabled = "AIDOP_SCHEDULER_ENABLED";
public const string EnvMasterEnabled = "AIDOP_S8_SCHEDULER_ENABLED";
/// 单 Job 配置键,形如 S8:WatchScheduler:Enabled。
public static string JobEnabledKey(S8BackgroundJob job) => $"S8:{SectionOf(job)}:Enabled";
/// 单 Job env override 名,形如 AIDOP_S8_WATCH_SCHEDULER_ENABLED。
public static string JobEnvName(S8BackgroundJob job) => job switch
{
S8BackgroundJob.WatchScheduler => "AIDOP_S8_WATCH_SCHEDULER_ENABLED",
S8BackgroundJob.TimeoutAutoEscalation => "AIDOP_S8_TIMEOUT_AUTO_ESCALATION_ENABLED",
S8BackgroundJob.ActiveFlowStuckScan => "AIDOP_S8_ACTIVE_FLOW_STUCK_SCAN_ENABLED",
_ => throw new ArgumentOutOfRangeException(nameof(job), job, null),
};
private static string SectionOf(S8BackgroundJob job) => job switch
{
S8BackgroundJob.WatchScheduler => "WatchScheduler",
S8BackgroundJob.TimeoutAutoEscalation => "TimeoutAutoEscalation",
S8BackgroundJob.ActiveFlowStuckScan => "ActiveFlowStuckScan",
_ => throw new ArgumentOutOfRangeException(nameof(job), job, null),
};
/// 求值三级开关。纯函数:不访问数据库、不解析租户、不产生副作用。
public static S8JobSwitchDecision Evaluate(IConfiguration configuration, S8BackgroundJob job)
{
ArgumentNullException.ThrowIfNull(configuration);
string? parseFailEnvName = null;
var environmentEnabled = ResolveBool(
configuration.GetValue(EnvironmentEnabledKey, true), EnvEnvironmentEnabled, ref parseFailEnvName);
var masterEnabled = ResolveBool(
configuration.GetValue(MasterEnabledKey, false), EnvMasterEnabled, ref parseFailEnvName);
var jobEnabled = ResolveBool(
configuration.GetValue(JobEnabledKey(job), false), JobEnvName(job), ref parseFailEnvName);
return new S8JobSwitchDecision(
EnvironmentEnabled: environmentEnabled,
MasterEnabled: masterEnabled,
JobEnabled: jobEnabled,
ParseFailEnvName: parseFailEnvName);
}
private static bool ResolveBool(bool fromJson, string envName, ref string? parseFailEnvName)
{
var raw = Environment.GetEnvironmentVariable(envName);
if (string.IsNullOrWhiteSpace(raw)) return fromJson;
if (bool.TryParse(raw, out var parsed)) return parsed;
parseFailEnvName ??= envName;
return fromJson;
}
}
/// 三级开关求值结果。 为 false 时调用方必须立即 return。
/// 环境级总开关 Scheduler:Enabled 的生效值。
/// S8 业务级总开关 S8:Scheduler:Enabled 的生效值。
/// 单 Job 开关 S8:{Job}:Enabled 的生效值。
/// 首个 bool.TryParse 失败的 env 名;无失败为 null。
public sealed record S8JobSwitchDecision(
bool EnvironmentEnabled,
bool MasterEnabled,
bool JobEnabled,
string? ParseFailEnvName)
{
/// 三级与:任一为 false 即不执行业务体。
public bool Enabled => EnvironmentEnabled && MasterEnabled && JobEnabled;
}