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},在新鲜度窗口内,无更新的失败或在途运行");
}
}