S8RuleCatalog.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
  4. /// <summary>规则目录:回答「这个 rule_code 在当前代码版本里是否被定义、定义成什么」。</summary>
  5. public interface IS8RuleCatalog
  6. {
  7. /// <summary>取定义;未定义返回 null。</summary>
  8. S8RuleDefinition? TryGet(string? ruleCode);
  9. /// <summary>取定义;未定义抛 <see cref="S8BizException"/>,消息带 reason code。</summary>
  10. S8RuleDefinition GetRequired(string? ruleCode);
  11. /// <summary>是否已定义。</summary>
  12. bool IsDefined(string? ruleCode);
  13. /// <summary>全部已定义规则。Batch 2 的 provisioning 将按此为每个租户落 runtime policy 行。</summary>
  14. IReadOnlyCollection<S8RuleDefinition> Definitions { get; }
  15. }
  16. /// <summary>
  17. /// 默认目录实现:聚合所有 <see cref="IS8RuleDefinitionSource"/>,并在构造时做完整性校验。
  18. ///
  19. /// <para><b>为什么校验放在构造函数而不是运行期</b>:一条定义写错(rule_type 不在词表、
  20. /// TIMEOUT 却没给 Timeout 语义、scene 不是 S1–S7)属于**发布事故**,
  21. /// 应该在服务起来的时候就炸掉,而不是等某条规则跑到那一 tick 才报错——
  22. /// 那时错误只出现在后台日志里,页面上一切正常。</para>
  23. ///
  24. /// <para><b>注册为 Singleton + 启动期强制解析</b>(见 <c>Startup.Configure</c>)。
  25. /// 数据集侧 <c>S8DatasetCatalog</c> 注册为 Transient,其重复定义的失败形态是
  26. /// 「首次解析该服务的那个请求 500」而非启动崩溃 —— 该问题在
  27. /// <c>S8BusinessDatasetDefinitions</c> 的注释里已被记录为已知弱点。规则侧不重复这个弱点。</para>
  28. /// </summary>
  29. public class S8RuleCatalog : IS8RuleCatalog
  30. {
  31. /// <summary>规则在当前代码版本中无定义。与「配置错了」严格区分:它意味着该规则不该运行。</summary>
  32. public const string ReasonNotFound = "rule_definition_not_found";
  33. private readonly Dictionary<string, S8RuleDefinition> _definitions;
  34. public S8RuleCatalog(IEnumerable<IS8RuleDefinitionSource> sources)
  35. {
  36. // rule_code 在 DB 侧是大小写敏感比较(dedup_key 逐字拼接),
  37. // 但目录查找用不敏感比较可以把「大小写写错」暴露成重复定义而不是静默两条,
  38. // 与 S8DatasetCatalog 的 OrdinalIgnoreCase 一致。
  39. _definitions = new Dictionary<string, S8RuleDefinition>(StringComparer.OrdinalIgnoreCase);
  40. foreach (var source in sources)
  41. {
  42. foreach (var definition in source.GetDefinitions())
  43. {
  44. Validate(definition, source.GetType().Name);
  45. var code = definition.RuleCode.Trim();
  46. if (_definitions.ContainsKey(code))
  47. throw new InvalidOperationException(
  48. $"监控规则 {code} 被重复定义(来源包含 {source.GetType().Name})");
  49. _definitions[code] = definition;
  50. }
  51. }
  52. }
  53. public IReadOnlyCollection<S8RuleDefinition> Definitions => _definitions.Values;
  54. public bool IsDefined(string? ruleCode) =>
  55. !string.IsNullOrWhiteSpace(ruleCode) && _definitions.ContainsKey(ruleCode!.Trim());
  56. public S8RuleDefinition? TryGet(string? ruleCode)
  57. {
  58. if (string.IsNullOrWhiteSpace(ruleCode)) return null;
  59. return _definitions.TryGetValue(ruleCode.Trim(), out var definition) ? definition : null;
  60. }
  61. public S8RuleDefinition GetRequired(string? ruleCode) =>
  62. TryGet(ruleCode) ?? throw new S8BizException(
  63. $"[{ReasonNotFound}] 监控规则 {(string.IsNullOrWhiteSpace(ruleCode) ? "(空)" : ruleCode)} 在当前版本中没有代码定义,无法配置或运行");
  64. /// <summary>
  65. /// 定义完整性校验。每一条都对应一种「存得下但跑不通」的历史故障形态,不是形式检查。
  66. /// </summary>
  67. private static void Validate(S8RuleDefinition d, string sourceName)
  68. {
  69. void Fail(string message) =>
  70. throw new InvalidOperationException($"监控规则定义非法(来源 {sourceName}):{message}");
  71. if (string.IsNullOrWhiteSpace(d.RuleCode)) Fail("rule_code 不能为空");
  72. if (string.IsNullOrWhiteSpace(d.DisplayName)) Fail($"{d.RuleCode} 缺少 DisplayName(配置页的规则名称没有其他来源)");
  73. if (string.IsNullOrWhiteSpace(d.DatasetCode)) Fail($"{d.RuleCode} 缺少 dataset_code");
  74. if (string.IsNullOrWhiteSpace(d.ExceptionTypeCode)) Fail($"{d.RuleCode} 缺少 exception_type_code");
  75. if (string.IsNullOrWhiteSpace(d.SourceObjectType)) Fail($"{d.RuleCode} 缺少 source_object_type(它参与 dedup_key)");
  76. // rule_type 必须在真实存在 evaluator 的词表内,且**大小写逐字相等**。
  77. // 刻意不用 S8RuleTypeFieldRequirements.IsKnown —— 它内部做 ToUpperInvariant,
  78. // 于是 "timeout" 能过校验;而调度器与 evaluator 的 switch 是 ordinal 敏感的,
  79. // 那条定义会一路存下来、最后在运行期报 unsupported_rule_type。
  80. // 「存得下但跑不通」正是本 Catalog 要消灭的形态。
  81. if (!S8RuleTypeFieldRequirements.KnownRuleTypes.Contains(d.RuleType, StringComparer.Ordinal))
  82. Fail($"{d.RuleCode} 的 rule_type 非法:{d.RuleType};当前仅支持 "
  83. + string.Join(" / ", S8RuleTypeFieldRequirements.KnownRuleTypes));
  84. // scene / stage 必须是严格 S1–S7,拒 legacy 复合场景(与 CreateAsync 既有口径一致)。
  85. if (!S8ModuleCode.IsValid(d.SceneCode))
  86. Fail($"{d.RuleCode} 的 scene_code 非法:{d.SceneCode};当前仅支持 " + string.Join(" / ", S8ModuleCode.All));
  87. if (!S8ModuleCode.IsValid(d.StageCode))
  88. Fail($"{d.RuleCode} 的 stage_code 非法:{d.StageCode}");
  89. // 类型与语义必须配套:声明 TIMEOUT 却不给 Timeout 语义,运行期只会表现为 NullReference。
  90. switch (d.RuleType)
  91. {
  92. case var t when t == S8TimeoutRuleEvaluator.RuleTypeCode:
  93. if (d.Timeout == null) Fail($"{d.RuleCode} 声明 TIMEOUT 但未提供 TIMEOUT 判定语义");
  94. break;
  95. case var t when t == S8ShortageRuleEvaluator.RuleTypeCode:
  96. if (d.Shortage == null) Fail($"{d.RuleCode} 声明 SHORTAGE 但未提供 SHORTAGE 判定语义");
  97. break;
  98. case var t when t == S8OutOfRangeRuleEvaluator.RuleTypeCode:
  99. if (d.OutOfRange == null) Fail($"{d.RuleCode} 声明 OUT_OF_RANGE 但未提供 OUT_OF_RANGE 判定语义");
  100. break;
  101. }
  102. var policy = d.Parameters ?? new S8RuleParameterPolicy();
  103. if (policy.AllowedSeverities == null || policy.AllowedSeverities.Count == 0)
  104. Fail($"{d.RuleCode} 的 AllowedSeverities 不能为空");
  105. if (!policy.AllowedSeverities!.Contains(policy.SeverityDefault, StringComparer.Ordinal))
  106. Fail($"{d.RuleCode} 的默认严重度 {policy.SeverityDefault} 不在允许集合内");
  107. }
  108. }