S8JobSwitch.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using Microsoft.Extensions.Configuration;
  2. namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  3. /// <summary>S8 后台自动任务标识。每个 Job 一个独立开关节。</summary>
  4. public enum S8BackgroundJob
  5. {
  6. /// <summary>S8 自动监控主链调度(<c>S8:WatchScheduler:Enabled</c>)。</summary>
  7. WatchScheduler,
  8. /// <summary>SLA 超时自动升级(<c>S8:TimeoutAutoEscalation:Enabled</c>)。</summary>
  9. TimeoutAutoEscalation,
  10. /// <summary>ActiveFlow 卡死扫描(<c>S8:ActiveFlowStuckScan:Enabled</c>)。</summary>
  11. ActiveFlowStuckScan,
  12. }
  13. /// <summary>
  14. /// S8-P0-2-PER-JOB-KILL-SWITCH-1:S8 后台任务三级开关求值。
  15. ///
  16. /// <para><b>为什么需要</b>:修复前三个 Job(WatchScheduler / TimeoutAutoEscalation / ActiveFlowStuckScan)
  17. /// 读的是**完全相同**的两个键 <c>Scheduler:Enabled</c> + <c>S8:Scheduler:Enabled</c>,
  18. /// 没有任何 per-job 键。于是「只想跑 Watch」在配置层不可表达 —— 2026-09-02 的受控启动里,
  19. /// 打开总开关同时放行了另外两个 Job,其中 <c>ActiveFlowStuckScan</c> 因带 <c>RunOnStart = true</c>
  20. /// 在重启瞬间就写入了 499 行告警日志。当时只能靠「先暂停 trigger 再重启」隔离,
  21. /// 而该手段实测**不可靠**(两次重启结果相反:一次暂停态丢失、一次存活)。</para>
  22. ///
  23. /// <para><b>生效语义</b>(三级与,任一 false 即停):</para>
  24. /// <code>
  25. /// Effective = Scheduler:Enabled // 环境级 master kill switch,默认 true
  26. /// &amp;&amp; S8:Scheduler:Enabled // S8 业务级 master kill switch,默认 false
  27. /// &amp;&amp; S8:{Job}:Enabled // 单 Job 开关,默认 false
  28. /// </code>
  29. ///
  30. /// <para><b>默认 false 是刻意的</b>:新增 per-job 键后,任何未显式声明该键的部署都保持不执行,
  31. /// 宁可「本该跑却没跑」也不要「本不该跑却跑了」——本模块的事故类型全部属于后者。</para>
  32. ///
  33. /// <para><b>env override</b>:Furion 4.9.8.24 把 <c>Configuration/*.json</c> 装配在 ASP.NET Core
  34. /// EnvironmentVariables provider **之后**,JSON 会盖掉 env,故所有开关都改用
  35. /// <see cref="Environment.GetEnvironmentVariable"/> 直读,命名沿用既有 <c>AIDOP_</c> 前缀约定。
  36. /// env 不存在 / 空白 → 沿用 JSON;存在但 <c>bool.TryParse</c> 失败 → 保留 JSON 值并回报
  37. /// <see cref="S8JobSwitchDecision.ParseFailEnvName"/>(由调用方做单次 warn,不输出 value)。</para>
  38. ///
  39. /// <para><b>与 RunOnStart 的关系</b>:本类只做纯计算、不碰 DB / service。调用方必须在
  40. /// <c>ExecuteAsync</c> 的**第一步**求值并在 false 时 <c>return</c>,因此即便
  41. /// <c>[Period(..., RunOnStart = true)]</c> 在启动瞬间触发,也会在进入任何业务扫描 / 写入之前退出。
  42. /// 不再需要、也不得依赖 trigger 暂停作为隔离手段。</para>
  43. /// </summary>
  44. public static class S8JobSwitch
  45. {
  46. /// <summary>环境级总开关配置键。默认 true(保持既有部署行为)。</summary>
  47. public const string EnvironmentEnabledKey = "Scheduler:Enabled";
  48. /// <summary>S8 业务级总开关配置键。默认 false。</summary>
  49. public const string MasterEnabledKey = "S8:Scheduler:Enabled";
  50. public const string EnvEnvironmentEnabled = "AIDOP_SCHEDULER_ENABLED";
  51. public const string EnvMasterEnabled = "AIDOP_S8_SCHEDULER_ENABLED";
  52. /// <summary>单 Job 配置键,形如 <c>S8:WatchScheduler:Enabled</c>。</summary>
  53. public static string JobEnabledKey(S8BackgroundJob job) => $"S8:{SectionOf(job)}:Enabled";
  54. /// <summary>单 Job env override 名,形如 <c>AIDOP_S8_WATCH_SCHEDULER_ENABLED</c>。</summary>
  55. public static string JobEnvName(S8BackgroundJob job) => job switch
  56. {
  57. S8BackgroundJob.WatchScheduler => "AIDOP_S8_WATCH_SCHEDULER_ENABLED",
  58. S8BackgroundJob.TimeoutAutoEscalation => "AIDOP_S8_TIMEOUT_AUTO_ESCALATION_ENABLED",
  59. S8BackgroundJob.ActiveFlowStuckScan => "AIDOP_S8_ACTIVE_FLOW_STUCK_SCAN_ENABLED",
  60. _ => throw new ArgumentOutOfRangeException(nameof(job), job, null),
  61. };
  62. private static string SectionOf(S8BackgroundJob job) => job switch
  63. {
  64. S8BackgroundJob.WatchScheduler => "WatchScheduler",
  65. S8BackgroundJob.TimeoutAutoEscalation => "TimeoutAutoEscalation",
  66. S8BackgroundJob.ActiveFlowStuckScan => "ActiveFlowStuckScan",
  67. _ => throw new ArgumentOutOfRangeException(nameof(job), job, null),
  68. };
  69. /// <summary>求值三级开关。纯函数:不访问数据库、不解析租户、不产生副作用。</summary>
  70. public static S8JobSwitchDecision Evaluate(IConfiguration configuration, S8BackgroundJob job)
  71. {
  72. ArgumentNullException.ThrowIfNull(configuration);
  73. string? parseFailEnvName = null;
  74. var environmentEnabled = ResolveBool(
  75. configuration.GetValue(EnvironmentEnabledKey, true), EnvEnvironmentEnabled, ref parseFailEnvName);
  76. var masterEnabled = ResolveBool(
  77. configuration.GetValue(MasterEnabledKey, false), EnvMasterEnabled, ref parseFailEnvName);
  78. var jobEnabled = ResolveBool(
  79. configuration.GetValue(JobEnabledKey(job), false), JobEnvName(job), ref parseFailEnvName);
  80. return new S8JobSwitchDecision(
  81. EnvironmentEnabled: environmentEnabled,
  82. MasterEnabled: masterEnabled,
  83. JobEnabled: jobEnabled,
  84. ParseFailEnvName: parseFailEnvName);
  85. }
  86. private static bool ResolveBool(bool fromJson, string envName, ref string? parseFailEnvName)
  87. {
  88. var raw = Environment.GetEnvironmentVariable(envName);
  89. if (string.IsNullOrWhiteSpace(raw)) return fromJson;
  90. if (bool.TryParse(raw, out var parsed)) return parsed;
  91. parseFailEnvName ??= envName;
  92. return fromJson;
  93. }
  94. }
  95. /// <summary>三级开关求值结果。<see cref="Enabled"/> 为 false 时调用方必须立即 return。</summary>
  96. /// <param name="EnvironmentEnabled">环境级总开关 <c>Scheduler:Enabled</c> 的生效值。</param>
  97. /// <param name="MasterEnabled">S8 业务级总开关 <c>S8:Scheduler:Enabled</c> 的生效值。</param>
  98. /// <param name="JobEnabled">单 Job 开关 <c>S8:{Job}:Enabled</c> 的生效值。</param>
  99. /// <param name="ParseFailEnvName">首个 <c>bool.TryParse</c> 失败的 env 名;无失败为 null。</param>
  100. public sealed record S8JobSwitchDecision(
  101. bool EnvironmentEnabled,
  102. bool MasterEnabled,
  103. bool JobEnabled,
  104. string? ParseFailEnvName)
  105. {
  106. /// <summary>三级与:任一为 false 即不执行业务体。</summary>
  107. public bool Enabled => EnvironmentEnabled && MasterEnabled && JobEnabled;
  108. }