diff --git a/plan.py b/plan.py index 81993bd..db20a6c 100644 --- a/plan.py +++ b/plan.py @@ -862,20 +862,22 @@ class StaleSnapshot(RuntimeError): def snapshot_fresh_enough(snap: dict) -> tuple: """这份快照够不够新,能不能当今天的计划直接下发。返回 (能不能, 一句原因)。 - 为什么需要这道守卫(2026-09-10,台账 057): 出计划这一步现在是**前一晚**跑的 - (实测 09-09 23:10 出的 09-09 那份),而数据基座凌晨才更新公司深度评析、催化事件 - 这些证据线。所以快照天生比实时装配旧半天 —— 实测两条路比对,代码序列一模一样, - 但候选卡名次、判决、依据这些字段有实质差异。直接拿旧快照下发,等于让下游用昨晚的判决。 + 为什么需要这道守卫(2026-09-10,台账 057): 快照是出计划那一刻的定格,而实时装配读的是 + 此刻的证据线。实测两条路比对,代码序列一模一样,但候选卡名次、判决、依据这些字段会有 + 实质差异 —— 快照出得越早、差得越多。拿一份过时的快照下发,等于让下游用旧判决。 - 两条判据,都不满足就退回实时装配(也就是今天的行为,不会变坏): - · 代码版本要和现在跑的一致 —— 版本变了说明判决逻辑可能变了,昨天算的不算数 - · 生成时刻要晚于今天的界(默认早上六点)—— 保证它是在数据基座凌晨那批任务之后出的 + 两条判据,任一不满足就退回实时装配(也就是这次改动之前的行为,不会变坏): + · 代码版本要和现在跑的一致。版本变了说明判决逻辑可能变了,用旧代码算的那份不算数。 + 正常情况下两者相同——出计划任务和接口进程跑的是同一份部署;只有"刚部署了新代码、 + 还没重新出计划"这个窗口里会不一致,而那正是该回落的时候。 + · 生成时刻距今不超过 PLAN_SNAPSHOT_MAX_AGE_HOURS 小时(默认十六)。 - **这道守卫要能松开,靠的是把出计划挪到数据基座之后**(比如早上七点十分,代码注释里 - 本来就是这么设计的),不是把界调低。界调低只会让旧快照蒙混过关。 + **时间判据用的是"距今多久",不是"晚于今天几点"**:容器跑在世界协调时上(实测 + 2026-09-10 早上七点十分出的计划,快照里记的是 2026-09-09T23:10),拿本地日期算界会 + 差八小时,把新鲜的快照判成过期。距今多久与时区无关,怎么部署都不会算错。 """ import version - min_hour = int(os.environ.get("PLAN_SNAPSHOT_MIN_HOUR", "6")) + max_age_h = float(os.environ.get("PLAN_SNAPSHOT_MAX_AGE_HOURS", "16")) gen = str(snap.get("generated_at") or "") ver = str(snap.get("plan_version") or "") now_ver = "" @@ -892,10 +894,12 @@ def snapshot_fresh_enough(snap: dict) -> tuple: g = dt.datetime.fromisoformat(gen) except ValueError: return False, f"快照的生成时刻认不出: {gen}" - today_line = dt.datetime.now().replace(hour=min_hour, minute=0, second=0, microsecond=0) - if g < today_line: - return False, (f"快照出于 {gen},早于今天 {min_hour:02d}:00;" - f"数据基座凌晨那批证据线更新它没赶上,本次按实时装配") + age_h = (dt.datetime.now() - g).total_seconds() / 3600.0 + if age_h > max_age_h: + return False, (f"快照出于 {gen},距今 {age_h:.1f} 小时,超过 {max_age_h:g} 小时的界;" + f"证据线可能已经变了,本次按实时装配") + if age_h < -1: # 生成时刻在未来:多半是两边时区不一致,别当新鲜的用 + return False, f"快照的生成时刻 {gen} 在未来 {-age_h:.1f} 小时,判不了新旧" return True, "" diff --git a/test_plan_snapshot.py b/test_plan_snapshot.py index bce21c1..6b92b98 100644 --- a/test_plan_snapshot.py +++ b/test_plan_snapshot.py @@ -127,18 +127,30 @@ def test_freshness_guard(): def snap(gen, ver=None): return {"generated_at": gen, "plan_version": cur if ver is None else ver} - fresh = now.replace(hour=7, minute=30, second=0, microsecond=0) - if now < fresh: # 早上七点半之前跑测试时,用「一分钟前」当新鲜样本 - fresh = now - _dt.timedelta(minutes=1) - ok, why = plan.snapshot_fresh_enough(snap(fresh.isoformat(timespec="seconds"))) - t("今天出的、版本对得上的快照可用", ok, why) + # 时间判据是「距今多久」,不是「晚于今天几点」——容器跑在世界协调时上, + # 拿本地日期算界会差八小时,把早上七点十分出的快照判成过期(2026-09-10 实测踩到) + ok, why = plan.snapshot_fresh_enough( + snap((now - _dt.timedelta(hours=2)).isoformat(timespec="seconds"))) + t("两小时前出的、版本对得上的快照可用", ok, why) - y = (now - _dt.timedelta(days=1)).replace(hour=23, minute=10) - ok2, why2 = plan.snapshot_fresh_enough(snap(y.isoformat(timespec="seconds"))) - t("昨晚出的快照不可用(数据基座凌晨那批它没赶上)", not ok2 and "早于今天" in why2, why2) + ok2, why2 = plan.snapshot_fresh_enough( + snap((now - _dt.timedelta(hours=30)).isoformat(timespec="seconds"))) + t("三十小时前的快照不可用", not ok2 and "距今" in why2, why2) + + ok_edge, _ = plan.snapshot_fresh_enough( + snap((now - _dt.timedelta(hours=15.5)).isoformat(timespec="seconds"))) + t("十五个半小时刚好在界内", ok_edge) + ok_edge2, _ = plan.snapshot_fresh_enough( + snap((now - _dt.timedelta(hours=16.5)).isoformat(timespec="seconds"))) + t("十六个半小时超界", not ok_edge2) + + ok_f, why_f = plan.snapshot_fresh_enough( + snap((now + _dt.timedelta(hours=5)).isoformat(timespec="seconds"))) + t("生成时刻在未来的不可用(多半是两边时区不一致)", not ok_f and "未来" in why_f, why_f) if cur: - ok3, why3 = plan.snapshot_fresh_enough(snap(fresh.isoformat(timespec="seconds"), "deadbee")) + ok3, why3 = plan.snapshot_fresh_enough( + snap((now - _dt.timedelta(hours=2)).isoformat(timespec="seconds"), "deadbee")) t("代码版本对不上就不可用(判决逻辑可能变了)", not ok3 and "代码版本" in why3, why3) ok4, _ = plan.snapshot_fresh_enough(snap(""))