新鲜度的时间判据改成「距今多久」,与时区无关

容器跑在世界协调时上:2026-09-10 早上七点十分出的计划,快照里记的是
2026-09-09T23:10。拿本地日期算「晚于今天六点」会差八小时,把新鲜的快照判成过期。
顺带更正一处判断:出计划本来就在数据基座之后跑,不是前一晚。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-10 09:33:19 +08:00
parent 3ec99a5462
commit 7ce2484abe
2 changed files with 39 additions and 23 deletions

32
plan.py
View File

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

View File

@ -127,18 +127,30 @@ def test_freshness_guard():
def snap(gen, ver=None): def snap(gen, ver=None):
return {"generated_at": gen, "plan_version": cur if ver is None else ver} 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: # 早上七点半之前跑测试时,用「一分钟前」当新鲜样本 # 拿本地日期算界会差八小时把早上七点十分出的快照判成过期2026-09-10 实测踩到)
fresh = now - _dt.timedelta(minutes=1) ok, why = plan.snapshot_fresh_enough(
ok, why = plan.snapshot_fresh_enough(snap(fresh.isoformat(timespec="seconds"))) snap((now - _dt.timedelta(hours=2)).isoformat(timespec="seconds")))
t("今天出的、版本对得上的快照可用", ok, why) t("两小时前出的、版本对得上的快照可用", ok, why)
y = (now - _dt.timedelta(days=1)).replace(hour=23, minute=10) ok2, why2 = plan.snapshot_fresh_enough(
ok2, why2 = plan.snapshot_fresh_enough(snap(y.isoformat(timespec="seconds"))) snap((now - _dt.timedelta(hours=30)).isoformat(timespec="seconds")))
t("昨晚出的快照不可用(数据基座凌晨那批它没赶上)", not ok2 and "早于今天" in why2, why2) 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: 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) t("代码版本对不上就不可用(判决逻辑可能变了)", not ok3 and "代码版本" in why3, why3)
ok4, _ = plan.snapshot_fresh_enough(snap("")) ok4, _ = plan.snapshot_fresh_enough(snap(""))