namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// 本实例「是否被指派为 ETL 执行机」的内存投影。由 /// 在每次心跳时发布,由 读取。 /// /// 为什么要这层投影,而不是让闸门直接查库: /// 的调用面是 21 个 IJob 加 2 个 Worker, /// 其中 ModuleRebuildWorker 每 5 秒轮询一次。若每次求值都查一次 /// ado_etl_instance,闸门自身就会变成新的连接压力源——那正是 2026-09-24 /// 连接耗尽事故的成因,等于把病因又造一遍。 /// 注册器本来就要按心跳周期写库,顺带把 is_runner 读回来发布到这里, /// 热路径便回到纯内存读,零额外查询。 /// /// 过期即未知:注册器若卡死或持续查库失败,这里的值会过期。 /// 超过 未刷新即视为「未知」,由调用方回退到配置值, /// 而不是继续用一个可能已被运维改掉的旧指派。 /// /// 纯内存 + volatile,可在任意线程反复读写。 /// public static class AidopRunnerState { /// /// 新鲜度窗口。取注册器心跳周期的 3 倍:允许偶发一两次心跳失败而不立刻退化为未知, /// 又能保证指派变更最迟在一个窗口内生效。 /// public static readonly TimeSpan FreshnessWindow = TimeSpan.FromSeconds(60); private static volatile bool _assigned; private static long _publishedAtTicks; // DateTime.UtcNow.Ticks;0 = 从未发布 /// 注册器成功读到本实例指派位后调用。 public static void Publish(bool assigned) { _assigned = assigned; Interlocked.Exchange(ref _publishedAtTicks, DateTime.UtcNow.Ticks); } /// /// 注册器查库失败时调用,立即把状态打回未知。 /// 不保留上一次的值:指派可能已经被改掉,继续沿用会让两台实例同时自认执行机。 /// public static void Invalidate() { Interlocked.Exchange(ref _publishedAtTicks, 0); } /// /// 取新鲜的指派值。返回 false 表示「未知」,调用方须回退到配置值。 /// public static bool TryGetFresh(out bool assigned) { assigned = false; var ticks = Interlocked.Read(ref _publishedAtTicks); if (ticks == 0) return false; var age = DateTime.UtcNow - new DateTime(ticks, DateTimeKind.Utc); if (age > FreshnessWindow) return false; assigned = _assigned; return true; } /// 仅供测试重置,勿在业务代码调用。 internal static void ResetForTests() => Interlocked.Exchange(ref _publishedAtTicks, 0); }