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