| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Health;
- /// <summary>快照发布不变量的观测结果。</summary>
- public static class S8SnapshotInvariant
- {
- /// <summary>该 Authority 是当前态镜像,无发布步骤 ⇒ 不适用。</summary>
- public const string NotApplicable = "NOT_APPLICABLE";
- /// <summary>恰好一个当前批次,且等于最近一次成功运行的批次;当前快照内无必填列为空。</summary>
- public const string Satisfied = "SATISFIED";
- /// <summary>零个当前批次 —— 合法空快照与发布失败在库里字面等同,无法区分。</summary>
- public const string Unconfirmed = "UNCONFIRMED";
- /// <summary>多个当前批次,或当前批次与最近成功运行不符,或当前快照内存在必填列为空。</summary>
- public const string Violated = "VIOLATED";
- }
- /// <summary>健康判定所需的全部观测值。由适配层采集,判定本身不碰数据库。</summary>
- public sealed class S8AuthorityObservation
- {
- public string DatasetCode { get; init; } = string.Empty;
- public long TenantId { get; init; }
- public DateTime ObservedAt { get; init; }
- /// <summary>数据集是否声明了 AuthoritySpec。</summary>
- public bool SpecFound { get; init; }
- /// <summary>见 <see cref="S8AuthoritySpec.ProducerTrusted"/>。</summary>
- public bool ProducerTrusted { get; init; } = true;
- public string UntrustedReasonCode { get; init; } = S8AuthorityHealthReason.Rule01MultiWriterUntrusted;
- /// <summary>回溯窗口内最近一次成功生产运行的<b>结束</b>时刻。null = 窗口内没有成功。</summary>
- public DateTime? LastSuccessEndAt { get; init; }
- /// <summary>最近一次成功之后是否出现了终态非成功的运行。</summary>
- public bool HasNewerTerminalFailure { get; init; }
- /// <summary>上一条的具体状态字面量(用于区分 LATEST_FAILED / LATEST_PARTIAL)。</summary>
- public string? NewerTerminalFailureStatus { get; init; }
- /// <summary>比最近一次成功更新、且尚未终态的运行中,最早那条的开始时刻。null = 没有在途运行。</summary>
- public DateTime? OldestNewerInFlightStartAt { get; init; }
- /// <summary>见 <see cref="S8SnapshotInvariant"/>。</summary>
- public string SnapshotInvariant { get; init; } = S8SnapshotInvariant.NotApplicable;
- /// <summary>快照不变量被破坏时的细节,写进 Reason 便于排障。</summary>
- public string? SnapshotDetail { get; init; }
- public TimeSpan StaleWindow { get; init; } = TimeSpan.FromHours(12);
- }
- /// <summary>
- /// Authority 健康判定的<b>纯函数核心</b>。
- ///
- /// <para>刻意不依赖数据库、时钟与任何服务:所有输入都在
- /// <see cref="S8AuthorityObservation"/> 里显式给出,因此 A–K 全部场景都能用普通单测覆盖,
- /// 不需要连库、不需要 <c>AIDOP_IT</c> 门控(该门控在未设变量时是静默 return、显示为绿,
- /// 核心判定绝不能只靠它守)。</para>
- ///
- /// <para><b>不变量</b>:本文件<b>不存在</b>任何行数入参,因此
- /// 「取数为空 ⇒ 拦截恢复」这条错误逻辑在结构上写不出来。</para>
- /// </summary>
- 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},在新鲜度窗口内,无更新的失败或在途运行");
- }
- }
|