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