namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Health; /// 快照发布不变量的观测结果。 public static class S8SnapshotInvariant { /// 该 Authority 是当前态镜像,无发布步骤 ⇒ 不适用。 public const string NotApplicable = "NOT_APPLICABLE"; /// 恰好一个当前批次,且等于最近一次成功运行的批次;当前快照内无必填列为空。 public const string Satisfied = "SATISFIED"; /// 零个当前批次 —— 合法空快照与发布失败在库里字面等同,无法区分。 public const string Unconfirmed = "UNCONFIRMED"; /// 多个当前批次,或当前批次与最近成功运行不符,或当前快照内存在必填列为空。 public const string Violated = "VIOLATED"; } /// 健康判定所需的全部观测值。由适配层采集,判定本身不碰数据库。 public sealed class S8AuthorityObservation { public string DatasetCode { get; init; } = string.Empty; public long TenantId { get; init; } public DateTime ObservedAt { get; init; } /// 数据集是否声明了 AuthoritySpec。 public bool SpecFound { get; init; } /// public bool ProducerTrusted { get; init; } = true; public string UntrustedReasonCode { get; init; } = S8AuthorityHealthReason.Rule01MultiWriterUntrusted; /// 回溯窗口内最近一次成功生产运行的结束时刻。null = 窗口内没有成功。 public DateTime? LastSuccessEndAt { get; init; } /// 最近一次成功之后是否出现了终态非成功的运行。 public bool HasNewerTerminalFailure { get; init; } /// 上一条的具体状态字面量(用于区分 LATEST_FAILED / LATEST_PARTIAL)。 public string? NewerTerminalFailureStatus { get; init; } /// 比最近一次成功更新、且尚未终态的运行中,最早那条的开始时刻。null = 没有在途运行。 public DateTime? OldestNewerInFlightStartAt { get; init; } /// public string SnapshotInvariant { get; init; } = S8SnapshotInvariant.NotApplicable; /// 快照不变量被破坏时的细节,写进 Reason 便于排障。 public string? SnapshotDetail { get; init; } public TimeSpan StaleWindow { get; init; } = TimeSpan.FromHours(12); } /// /// Authority 健康判定的纯函数核心。 /// /// 刻意不依赖数据库、时钟与任何服务:所有输入都在 /// 里显式给出,因此 A–K 全部场景都能用普通单测覆盖, /// 不需要连库、不需要 AIDOP_IT 门控(该门控在未设变量时是静默 return、显示为绿, /// 核心判定绝不能只靠它守)。 /// /// 不变量:本文件不存在任何行数入参,因此 /// 「取数为空 ⇒ 拦截恢复」这条错误逻辑在结构上写不出来。 /// public static class S8AuthorityHealthEvaluator { public static S8AuthorityHealthResult Evaluate(S8AuthorityObservation o) { ArgumentNullException.ThrowIfNull(o); var staleAfter = o.LastSuccessEndAt.HasValue ? o.LastSuccessEndAt.Value + o.StaleWindow : (DateTime?)null; S8AuthorityHealthResult Result(string state, string reasonCode, string reason) => new() { DatasetCode = o.DatasetCode, TenantId = o.TenantId, State = state, ReasonCode = reasonCode, Reason = reason, ObservedAt = o.ObservedAt, LastSuccessAt = o.LastSuccessEndAt, StaleAfter = staleAfter }; // ① 没有声明 AuthoritySpec —— 无从判断,保守拦截。 if (!o.SpecFound) return Result(S8AuthorityHealthState.Unknown, S8AuthorityHealthReason.NoAuthoritySpec, $"数据集 {o.DatasetCode} 未声明 AuthoritySpec,无法判定 Authority 健康度"); // ② 生产者不可信(多写入方且部分不留运行日志)—— run-log 证明不了 provenance。 if (!o.ProducerTrusted) return Result(S8AuthorityHealthState.Unknown, o.UntrustedReasonCode, $"数据集 {o.DatasetCode} 的 Authority 存在多个写入方且部分不写运行日志,运行日志无法证明数据出处"); // ③ 回溯窗口内没有任何成功运行。含「从未同步」与「停摆超过回溯窗口」。 if (!o.LastSuccessEndAt.HasValue) return Result(S8AuthorityHealthState.Unknown, S8AuthorityHealthReason.NoSuccessfulRun, $"回溯窗口内没有 {o.DatasetCode} 的成功生产运行"); // ④ 最近一次成功之后出现终态失败 —— 之后的数据状态不可信。 // 先于 STALE 判定,因为「失败」比「过期」更具体、更有排障价值。 if (o.HasNewerTerminalFailure) { var isPartial = string.Equals(o.NewerTerminalFailureStatus, "PARTIAL", StringComparison.OrdinalIgnoreCase); return Result(S8AuthorityHealthState.Degraded, isPartial ? S8AuthorityHealthReason.LatestPartial : S8AuthorityHealthReason.LatestFailed, $"最近一次成功({o.LastSuccessEndAt:yyyy-MM-dd HH:mm:ss})之后出现终态 " + $"{o.NewerTerminalFailureStatus ?? "非成功"} 运行"); } // ⑤ 存在比最近一次成功更新、且尚未终态的运行 —— 本轮 Authority 可能只写了一半。 // IN_FLIGHT 与 STUCK_RUN 都拦截,区别只在排障动作(等它跑完 / 去清孤儿行)。 if (o.OldestNewerInFlightStartAt.HasValue) { var age = o.ObservedAt - o.OldestNewerInFlightStartAt.Value; var stuck = age > o.StaleWindow; return Result(S8AuthorityHealthState.Degraded, stuck ? S8AuthorityHealthReason.StuckRun : S8AuthorityHealthReason.InFlight, $"存在比最近一次成功更新且未终态的生产运行(开始于 " + $"{o.OldestNewerInFlightStartAt:yyyy-MM-dd HH:mm:ss},已历时 {age.TotalMinutes:F0} 分钟)"); } // ⑥ 最近一次成功已过期。 if (o.ObservedAt > staleAfter!.Value) return Result(S8AuthorityHealthState.Degraded, S8AuthorityHealthReason.Stale, $"最近一次成功于 {o.LastSuccessEndAt:yyyy-MM-dd HH:mm:ss}," + $"已超过 {o.StaleWindow.TotalHours:F1} 小时的新鲜度窗口"); // ⑦ 快照不变量被破坏。 if (string.Equals(o.SnapshotInvariant, S8SnapshotInvariant.Violated, StringComparison.Ordinal)) return Result(S8AuthorityHealthState.Degraded, S8AuthorityHealthReason.SnapshotInvariantFailed, $"当前快照不变量被破坏:{o.SnapshotDetail ?? "(无细节)"}"); // ⑧ 快照发布无法确认 —— 合法空快照与发布失败字面等同,不猜,判 UNKNOWN。 if (string.Equals(o.SnapshotInvariant, S8SnapshotInvariant.Unconfirmed, StringComparison.Ordinal)) return Result(S8AuthorityHealthState.Unknown, S8AuthorityHealthReason.SnapshotPublicationUnconfirmed, $"无法确认本轮快照是否发布成功:{o.SnapshotDetail ?? "当前批次数为 0"}"); // ⑨ 通过。注意:走到这里从未读过任何行数 —— Authority 可信但一行都没有,同样是 HEALTHY。 return Result(S8AuthorityHealthState.Healthy, S8AuthorityHealthReason.Healthy, $"最近一次成功于 {o.LastSuccessEndAt:yyyy-MM-dd HH:mm:ss},在新鲜度窗口内,无更新的失败或在途运行"); } }