| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 |
- namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Health;
- /// <summary>Authority 的快照语义种类。决定除「生产运行是否成功」之外还要不要校验表级不变量。</summary>
- public static class S8AuthorityKind
- {
- /// <summary>当前态镜像表(如 <c>mdp_std_so</c>):upsert 覆盖、无发布步骤、读者不按批次取数 ⇒ 无表级不变量可校验。</summary>
- public const string CurrentStateMirror = "CURRENT_STATE_MIRROR";
- /// <summary>当前快照表(如 <c>dwd_requirement_examine_detail</c>):按批次累积 + 末尾原子翻牌发布 ⇒ 必须校验发布不变量。</summary>
- public const string PublishedSnapshot = "PUBLISHED_SNAPSHOT";
- }
- /// <summary>
- /// 数据集的 Authority 声明:健康判定需要知道的一切,全部声明在数据集定义处。
- ///
- /// <para><b>为什么放在数据集定义而不是规则上</b>:不变量是 per-dataset 的。
- /// Rule02 与 Rule03 共用同一个生产者作业(实测 <c>job_code</c> 同为 <c>S1_MDP_SYNC_TRANSFORM</c>、
- /// 同一次 run、同一个 batch_id),仅靠运行日志无法区分二者;把不变量挂在数据集上,
- /// Resolver 内部就不需要 <c>switch (ruleCode)</c>。</para>
- ///
- /// <para><b>调度器不感知物理表名</b>:它只传 <c>DatasetCode</c>,
- /// 由 MDP 适配实现去查具体的表与运行日志。</para>
- /// </summary>
- public sealed class S8AuthoritySpec
- {
- /// <summary>生产该 Authority 的转换作业编码,对应 <c>mdp_transform_run_log.job_code</c>。</summary>
- public string ProducerJobCode { get; init; } = string.Empty;
- /// <summary>见 <see cref="S8AuthorityKind"/>。</summary>
- public string AuthorityKind { get; init; } = S8AuthorityKind.CurrentStateMirror;
- /// <summary>
- /// 最近一次成功距今超过该时长即判 STALE。
- ///
- /// <para><b>必须显式声明,不从作业周期推导</b>。实测四条理由:
- /// ① 声明周期 60 分钟,而同租户实际相邻成功间隔为 28–39 分钟,声明值高估;
- /// ② 近 14 天 3530 个间隔中,最大 8.04 小时、超过 3 小时的有 46 次 ——
- /// 若按「3 × 周期 = 3 小时」判定,14 天内会产生 46 次假 STALE;
- /// ③ 触发器调度的是「入队」而非「执行」,每租户实际起跑时间还叠加了不可控的队列延迟;
- /// ④ 触发器 Args 的单位与形态不统一(PeriodTrigger 与 CronTrigger 混存,
- /// <c>[Period(ms)]</c> 与 <c>[PeriodSeconds(s)]</c> 都序列化成裸数字)。
- /// 且该表可被作业管理界面在运行期改写,不应让门禁阈值随之漂移。</para>
- /// </summary>
- public TimeSpan StaleWindow { get; init; } = TimeSpan.FromHours(12);
- /// <summary>
- /// 查找「最近一次成功」时的回溯窗口。<b>不是可选优化</b>:
- /// 实测无界回溯在长期停摆的租户上单次耗时 146ms(反向扫描 7122 条索引项),
- /// 加 7 天上界后降到 7.3ms。且语义上等价 —— 「回溯窗口内没有成功」本身就是 STALE 结论。
- /// </summary>
- public TimeSpan LookbackWindow { get; init; } = TimeSpan.FromDays(7);
- /// <summary>
- /// 生产者是否可信。
- /// <para>false 表示该 Authority 存在多个写入方、且部分写入方不写运行日志,
- /// 于是 run-log 无法证明表里最终的数据出自谁 —— 此时一律判 UNKNOWN、拦截恢复。
- /// 这是 Rule01 的现状(<c>dwd_supplier_delivery</c> 除 MDP 转换外,
- /// 还有两个 API 路径直写且不留 run-log)。</para>
- /// </summary>
- public bool ProducerTrusted { get; init; } = true;
- /// <summary>ProducerTrusted 为 false 时的原因码,见 <see cref="S8AuthorityHealthReason"/>。</summary>
- public string UntrustedReasonCode { get; init; } = S8AuthorityHealthReason.Rule01MultiWriterUntrusted;
- // ── 以下仅 AuthorityKind = PUBLISHED_SNAPSHOT 时使用 ──────────────────────────────
- // 声明在这里而不是写死在 Resolver 里,是为了让 Resolver 保持通用:
- // 它只按声明去查,内部不出现 switch (datasetCode) 或 switch (tableName)。
- // 调度器仍然只传 DatasetCode,不接触任何物理表名。
- /// <summary>快照表名。</summary>
- public string? SnapshotTable { get; init; }
- /// <summary>快照表里标识批次的列。</summary>
- public string SnapshotBatchColumn { get; init; } = "calc_batch_id";
- /// <summary>快照表里标识「属于当前批次」的列(值为 1 即当前)。</summary>
- public string SnapshotCurrentFlagColumn { get; init; } = "is_current_flag";
- /// <summary>
- /// 当前快照内不允许为空的列。
- /// <para>用<b>绝对计数</b>校验(<c>COUNT(*) WHERE 当前 AND 列 IS NULL</c> 必须为 0),
- /// 而不是空值率 —— 计数在空快照上天然真空成立,与 HEALTHY_EMPTY 零冲突;
- /// 写成比率则会在空集上 0 除、语义未定义。</para>
- /// <para>只填「结构上不可能为空」的列。例如 <c>bom_level</c> 就<b>不能</b>放进来 ——
- /// 转换侧对非数字层级刻意留 NULL("脏值留 NULL,不猜层级"),把它列为必填等于按设计制造假 DEGRADED。</para>
- /// </summary>
- public string[] SnapshotRequiredColumns { get; init; } = Array.Empty<string>();
- }
|