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