|
@@ -57,9 +57,24 @@ public class S8WatchSchedulerService : ITransient
|
|
|
private const string DetectResultRecoveryBlocked = "RECOVERY_BLOCKED";
|
|
private const string DetectResultRecoveryBlocked = "RECOVERY_BLOCKED";
|
|
|
|
|
|
|
|
// S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细写入限频窗口。
|
|
// S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细写入限频窗口。
|
|
|
- // 同 (tenant, factory, rule_code, dedup_key) 在 10 分钟内最多写一条 REFRESHED detection_log;
|
|
|
|
|
|
|
+ // 同 (tenant, factory, rule_code, dedup_key) 在该窗口内最多写一条 REFRESHED detection_log;
|
|
|
// 业务侧 RefreshDetectionAsync / BackfillLegacyExceptionAsync 不受限频影响。
|
|
// 业务侧 RefreshDetectionAsync / BackfillLegacyExceptionAsync 不受限频影响。
|
|
|
- private const int RefreshedRateLimitMinutes = 10;
|
|
|
|
|
|
|
+ //
|
|
|
|
|
+ // S8-DETECTION-LOG-GROWTH-P0-B(2026-09-23):默认窗口由 10 → 60 分钟,并改为可配置。
|
|
|
|
|
+ // 口径提醒:限频判据是闭区间(DetectedAt >= cutoff),而业务 tick 为 5 分钟
|
|
|
|
|
+ // (S8:Scheduler:WatchTickIntervalMs 默认 300000),因此**实际写入周期 = 窗口 + 一个 tick**。
|
|
|
|
|
+ // 旧值 10 分钟实测跑成 15 分钟;新值 60 分钟实测应落在 65 分钟。
|
|
|
|
|
+ // 该窗口只影响 detection_log 明细密度,不影响 exception.last_detected_at /
|
|
|
|
|
+ // consecutive_hit_count / recovered_at / 通知链路——它们走 RefreshDetectionAsync,不经本限频。
|
|
|
|
|
+ private const int DefaultRefreshedRateLimitMinutes = 60;
|
|
|
|
|
+
|
|
|
|
|
+ // 优先级:环境变量 > 默认值(与 S8EvaluatorGuard.ResolveIntFromEnv 同一套 idiom)。
|
|
|
|
|
+ // 下限 1 分钟:仅防御 0 / 负数把限频整个关掉,不改变任何正常取值下的行为。
|
|
|
|
|
+ private const string EnvRefreshedRateLimitMinutes = "AIDOP_S8_REFRESHED_RATE_LIMIT_MINUTES";
|
|
|
|
|
+ private const int MinRefreshedRateLimitMinutes = 1;
|
|
|
|
|
+
|
|
|
|
|
+ // env 解析失败 warn 单进程内只输出一次,避免每 tick 刷日志。
|
|
|
|
|
+ private static int _firstRefreshedRateLimitParseFailLogged;
|
|
|
|
|
|
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
private const string DefaultTriggerType = "VALUE_DEVIATION";
|
|
|
private const string SqlDataSourceType = "SQL";
|
|
private const string SqlDataSourceType = "SQL";
|
|
@@ -593,9 +608,30 @@ public class S8WatchSchedulerService : ITransient
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// S8-DETECTION-LOG-GROWTH-P0-B:解析 REFRESHED 限频窗口(分钟)。
|
|
|
|
|
+ /// 优先级 env <c>AIDOP_S8_REFRESHED_RATE_LIMIT_MINUTES</c> > 默认 60。
|
|
|
|
|
+ /// env 缺失/空白 → 默认值;解析失败或小于下限 → 默认值 + 单次 warn(不输出 raw 值)。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private int ResolveRefreshedRateLimitMinutes()
|
|
|
|
|
+ {
|
|
|
|
|
+ var raw = Environment.GetEnvironmentVariable(EnvRefreshedRateLimitMinutes);
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(raw)) return DefaultRefreshedRateLimitMinutes;
|
|
|
|
|
+ if (int.TryParse(raw.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v)
|
|
|
|
|
+ && v >= MinRefreshedRateLimitMinutes)
|
|
|
|
|
+ return v;
|
|
|
|
|
+ if (Interlocked.Exchange(ref _firstRefreshedRateLimitParseFailLogged, 1) == 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning(
|
|
|
|
|
+ "s8_refreshed_ratelimit_env_invalid env={Env} fallback={Fallback}",
|
|
|
|
|
+ EnvRefreshedRateLimitMinutes, DefaultRefreshedRateLimitMinutes);
|
|
|
|
|
+ }
|
|
|
|
|
+ return DefaultRefreshedRateLimitMinutes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细限频判定。
|
|
/// S8-DETECTION-LOG-WRITE-REDUCE-P1-1:REFRESHED 明细限频判定。
|
|
|
- /// 查询 cutoff = detectedAt - RefreshedRateLimitMinutes 之后,
|
|
|
|
|
|
|
+ /// 查询 cutoff = detectedAt - 限频窗口(见 <see cref="ResolveRefreshedRateLimitMinutes"/>)之后,
|
|
|
/// 是否已有同 (tenant_id, factory_id, rule_code, dedup_key, DetectResult=REFRESHED) 的 detection_log;
|
|
/// 是否已有同 (tenant_id, factory_id, rule_code, dedup_key, DetectResult=REFRESHED) 的 detection_log;
|
|
|
/// 命中既有索引 idx_s8_detection_log_dedup_time。查询失败仅 LogWarning 后返回 false,
|
|
/// 命中既有索引 idx_s8_detection_log_dedup_time。查询失败仅 LogWarning 后返回 false,
|
|
|
/// 宁可多写一条 REFRESHED 明细,也不阻断调度主链路;ruleCode / dedupKey 空白时同样返回 false(保留写入)。
|
|
/// 宁可多写一条 REFRESHED 明细,也不阻断调度主链路;ruleCode / dedupKey 空白时同样返回 false(保留写入)。
|
|
@@ -607,7 +643,7 @@ public class S8WatchSchedulerService : ITransient
|
|
|
if (string.IsNullOrWhiteSpace(ruleCode) || string.IsNullOrWhiteSpace(dedupKey))
|
|
if (string.IsNullOrWhiteSpace(ruleCode) || string.IsNullOrWhiteSpace(dedupKey))
|
|
|
return false;
|
|
return false;
|
|
|
|
|
|
|
|
- var cutoff = detectedAt.AddMinutes(-RefreshedRateLimitMinutes);
|
|
|
|
|
|
|
+ var cutoff = detectedAt.AddMinutes(-ResolveRefreshedRateLimitMinutes());
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
return await _detectionLogRep.AsQueryable()
|
|
return await _detectionLogRep.AsQueryable()
|