AidopRunnerState.cs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. namespace Admin.NET.Plugin.AiDOP.Infrastructure;
  2. /// <summary>
  3. /// 本实例「是否被指派为 ETL 执行机」的内存投影。由 <see cref="EtlInstanceRegistrar"/>
  4. /// 在每次心跳时发布,由 <see cref="AidopJobGate"/> 读取。
  5. ///
  6. /// <para><b>为什么要这层投影,而不是让闸门直接查库</b>:
  7. /// <see cref="AidopJobGate.ShouldRun"/> 的调用面是 21 个 <c>IJob</c> 加 2 个 Worker,
  8. /// 其中 <c>ModuleRebuildWorker</c> 每 5 秒轮询一次。若每次求值都查一次
  9. /// <c>ado_etl_instance</c>,闸门自身就会变成新的连接压力源——那正是 2026-09-24
  10. /// 连接耗尽事故的成因,等于把病因又造一遍。
  11. /// 注册器本来就要按心跳周期写库,顺带把 <c>is_runner</c> 读回来发布到这里,
  12. /// 热路径便回到纯内存读,零额外查询。</para>
  13. ///
  14. /// <para><b>过期即未知</b>:注册器若卡死或持续查库失败,这里的值会过期。
  15. /// 超过 <see cref="FreshnessWindow"/> 未刷新即视为「未知」,由调用方回退到配置值,
  16. /// 而不是继续用一个可能已被运维改掉的旧指派。</para>
  17. ///
  18. /// <para>纯内存 + <c>volatile</c>,可在任意线程反复读写。</para>
  19. /// </summary>
  20. public static class AidopRunnerState
  21. {
  22. /// <summary>
  23. /// 新鲜度窗口。取注册器心跳周期的 3 倍:允许偶发一两次心跳失败而不立刻退化为未知,
  24. /// 又能保证指派变更最迟在一个窗口内生效。
  25. /// </summary>
  26. public static readonly TimeSpan FreshnessWindow = TimeSpan.FromSeconds(60);
  27. private static volatile bool _assigned;
  28. private static long _publishedAtTicks; // DateTime.UtcNow.Ticks;0 = 从未发布
  29. /// <summary>注册器成功读到本实例指派位后调用。</summary>
  30. public static void Publish(bool assigned)
  31. {
  32. _assigned = assigned;
  33. Interlocked.Exchange(ref _publishedAtTicks, DateTime.UtcNow.Ticks);
  34. }
  35. /// <summary>
  36. /// 注册器查库失败时调用,立即把状态打回未知。
  37. /// 不保留上一次的值:指派可能已经被改掉,继续沿用会让两台实例同时自认执行机。
  38. /// </summary>
  39. public static void Invalidate()
  40. {
  41. Interlocked.Exchange(ref _publishedAtTicks, 0);
  42. }
  43. /// <summary>
  44. /// 取新鲜的指派值。返回 false 表示「未知」,调用方须回退到配置值。
  45. /// </summary>
  46. public static bool TryGetFresh(out bool assigned)
  47. {
  48. assigned = false;
  49. var ticks = Interlocked.Read(ref _publishedAtTicks);
  50. if (ticks == 0)
  51. return false;
  52. var age = DateTime.UtcNow - new DateTime(ticks, DateTimeKind.Utc);
  53. if (age > FreshnessWindow)
  54. return false;
  55. assigned = _assigned;
  56. return true;
  57. }
  58. /// <summary>仅供测试重置,勿在业务代码调用。</summary>
  59. internal static void ResetForTests() => Interlocked.Exchange(ref _publishedAtTicks, 0);
  60. }