using System.Diagnostics; using System.Reflection; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// 本进程在 ETL 执行机注册表(ado_etl_instance)中的身份。 /// /// 为什么要进程级唯一:同一台机器可能同时跑多个后端进程(开发时的 /// dotnet run + 已发布的服务),它们连的是同一个库。若只用机器名做标识, /// 两个进程会抢同一行注册记录,指派给谁都说不清。故标识取 /// 机器名:进程ID:启动时间 三段,进程重启后必然产生新行。 /// /// 为什么用启动时间而不是随机 GUID:运维要在页面上认出「这是哪台、什么时候起的」。 /// GUID 每次重启都变且不可读,排障时无法和服务器上的进程对上。 /// /// 身份与角色是两件事: 只回答「我是哪次进程」, /// 执行机角色则指派给 这个跨重启稳定的部署槽位 /// (见 AdoEtlRunnerDesignation)。把角色挂在进程级标识上会让每次重启都丢指派。 /// /// 静态只读,进程生命周期内不变,可在任意线程反复读取。 /// public static class AidopInstanceIdentity { static AidopInstanceIdentity() { MachineName = SafeGet(() => Environment.MachineName, "unknown-host"); ProcessId = SafeGet(() => Environment.ProcessId, 0); StartedAt = SafeGet(() => Process.GetCurrentProcess().StartTime, DateTime.Now); AppVersion = SafeGet( () => Assembly.GetEntryAssembly()?.GetName().Version?.ToString(), null); // 长度受 ado_etl_instance.instance_id 的 200 字符约束;机器名过长时截断, // 进程ID + 启动时间仍能保证同机不同进程互不覆盖。 var host = MachineName.Length > 120 ? MachineName[..120] : MachineName; InstanceId = $"{host}:{ProcessId}:{StartedAt:yyyyMMddHHmmss}"; } /// 注册表主键语义列,形如 DESKTOP-ABC:12345:20260925071200。 public static string InstanceId { get; } public static string MachineName { get; } public static int ProcessId { get; } /// 本进程启动时间。取不到时退化为静态构造时刻,不影响唯一性。 public static DateTime StartedAt { get; } /// 入口程序集版本,仅供页面展示与排障;取不到为 null。 public static string AppVersion { get; } /// 配置键:本进程所属的 ETL 执行机槽位。 public const string SlotConfigKey = "AiDOP:Jobs:Slot"; /// 环境变量名:优先级高于配置,便于容器与服务单元各自声明。 public const string SlotEnvName = "AIDOP_ETL_SLOT"; /// /// 本进程所属的执行机槽位;未声明则为 null,**此时本进程永远不可能成为执行机**。 /// /// 这比旧模型更严:旧模型下任何一台注册上来的进程都能被指派, /// 开发机被指派正是 2026-09-24 连接耗尽事故的成因之一。未声明槽位者连候选都不是。 /// /// 槽位不同于「配置里写自己是不是执行机」:配置只声明身份(我是哪个槽位), /// 唯一权威仍在库里那条指派记录。这保留了 2026-09-25 治理的核心—— /// 单一开关、页面可见、热切换无需重启——只是让指派能跨重启存活。 /// /// 为什么惰性求值:静态构造可能早于宿主配置就绪, /// 那时读 App.GetConfig 会抛,若在构造里一次性定下来就会永久退化成 null。 /// 首次访问来自心跳(启动后 5 秒),此时配置一定已就绪。 /// public static string SlotCode => _slotCode.Value; private static readonly Lazy _slotCode = new(ResolveSlotCode, LazyThreadSafetyMode.ExecutionAndPublication); private static string ResolveSlotCode() { var fromEnv = SafeGet(() => Environment.GetEnvironmentVariable(SlotEnvName), null); if (!string.IsNullOrWhiteSpace(fromEnv)) return Normalize(fromEnv); var fromConfig = SafeGet(() => Furion.App.GetConfig(SlotConfigKey, true), null); return string.IsNullOrWhiteSpace(fromConfig) ? null : Normalize(fromConfig); } /// 槽位码要能被人工比对,故统一去空白并折成小写,长度受 128 字符列约束。 private static string Normalize(string raw) { var v = raw.Trim().ToLowerInvariant(); return v.Length > 128 ? v[..128] : v; } /// /// 身份计算不得成为启动故障点:容器内取进程启动时间、受限环境取机器名都可能抛异常, /// 一旦抛出会让整个插件装配失败,而这里拿到的只是展示用信息。 /// private static T SafeGet(Func get, T fallback) { try { return get(); } catch { return fallback; } } }