fix(deploy): 占位行判据改用**行的年龄**(阈值 1 会把健康并发判成泄漏)+ 后置验证补 binrev==HEAD
两件都来自 pi 2026-09-25 的实测,我逐条复跑确认。
## ① `占位行 <= 1` 是阈值版假红(pi 127746b5,我复现)
`ClaimRelay`(INSERT 占位) 与 `BindRelayMail`(UPDATE 绑定) **不在一个事务里** ⇒
"同时有 2 个请求在飞"的正常形态**就是** 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。
变异(/tmp/pl/t.db:清掉化石 + 插 2 个**合法在飞**占位)
旧判据: PLACE=2 ⇒ rc=**1** [FAIL] 占位行 <= 1 ← 把健康并发判成泄漏
新判据: rc=**0**,打印"最老 = 0 分钟;其中 >10 分钟 = 0"
阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。
⇒ 与泄漏正交的判别量是**行的年龄**(pi 给的):正常占位行只活一个请求的时长,
泄漏行活到**下一次请求之后**。年龄与并发度无关,也与"此刻是否恰好有请求"无关。
阈值 10 分钟:远大于任何单请求(HTTP 有超时),远小于真实泄漏尺度(那行活了 12.7 天)。
断言**上界**而非"==0"—— 0 是"恰好没请求在飞",不是健康的条件。
正反两面都验(只验一面会放过退化):
健康并发 2 行 → rc=0 ✓ 13 天泄漏 + 1 健康行 → rc=**1** ✓
真库 → rc=1(那行确实 18259 分钟 ⇒ **真阳性**,非假红)
并打印年龄 ⇒ 顺带答出"泄漏了多久"(旧判据只说"多了一行")。
## ② 抑制数丢了 keying(pi dd579a8e,我复核确认)
生成器键是 `(ag, prefix(k), r)`(**已含 agent**),但**序列化/打印时把 agent 丢了** ⇒
任何人引用这个数都会以为是"仅根"口径。实测两种 keying 全局差 6 封(61/37 vs 55/43,
6 组**全是跨 agent 合并**)⇒ 名字里不写,下一个人**必重犯**。
文本: `抑制数 [keying=(agent,根) scope=全局]: …`
JSON: `suppress_full_groups_keying_agent_root_scope_global`
★ 同时补 **scope**:本脚本报的是全局;session 侧同 keying 是另一个数(4/18)——
同 keying 两个 scope 两个数,少写任一个都只能靠猜。
★ 只改名字,**数的值不动**(这是防再犯,不是纠错)。
## ③ 后置验证补 `binrev == HEAD`(pi 127746b5 §一,落在脚本而非只落检查器)
§7 那几项(服务 active / `/health` 200 / 无 panic / SSE 重连)**全都与代码版本无关** ⇒
**旧二进制跑这套清单会全绿**。2026-09-25 实证:线上是 09-19 的二进制而 §7 全过。
pi 建议落在 `redeploy-gateway.sh`,我采纳 —— 因为我昨天已把同一件事实现为
`check-deploy-drift` 判据 ⑤b,但**那个检查器没有任何自动入口**(要人手跑);
落在部署脚本里才能"装完当场就判"。
三态与 ⑤b 同口径:相等→绿 / 不等→**红**(就地回滚)/ 取不到任一侧→**不判**且必须说出来
(判红会让 `-buildvcs=false`、老构建、非 git 构建恒红)。
实测逻辑: 线上 e8b260dd vs HEAD ⇒ 红 ✓;/nonexistent/binary ⇒ warn 不判 ✓
## 验证
`bash -n` 两文件通过;`recount-relay-counts.sh --json` 合法(23 键);
`criteria-hygiene.test.mjs` 7/7 通过(改名后仍满足 check-*.sh 命名约定)。
This commit is contained in:
@ -178,8 +178,12 @@ if [ "$JSON" = "1" ]; then
|
||||
printf '"perm_with_failure":%s,"summary_with_failure":%s,' "$PERM_FAIL" "$SUMMARY_FAIL"
|
||||
printf '"perm_without_request":%s,"summary_with_request":%s,' "$PERM_NO_REQ" "$SUM_WITH_REQ"
|
||||
printf '"loops_full":%s,"loops_fail":%s,' "$L_FULL" "$L_FAIL"
|
||||
printf '"suppress_full_groups":%s,"suppress_full":%s,' "$S_FULL_G" "$S_FULL_S"
|
||||
printf '"suppress_fail_groups":%s,"suppress_fail":%s}\n' "$S_FAIL_G" "$S_FAIL_S"
|
||||
# ★ 键名里必须带 **keying**(pi 2026-09-25 实测:生成器用了 `(ag, prefix(k), r)`,
|
||||
# 但序列化时把 `ag` 丢了 ⇒ 任何人引用这个数都会以为它是"仅根"口径)。
|
||||
# 实测两种 keying 全局差 6 封(37/6 vs 43/10,6 组全是跨 agent 合并)⇒ 名字里不写,
|
||||
# 下一个人**必重犯**。scope 同为必需:本脚本报的是**全局**;session 侧同 keying 是另一个数。
|
||||
printf '"suppress_full_groups_keying_agent_root_scope_global":%s,"suppress_full":%s,' "$S_FULL_G" "$S_FULL_S"
|
||||
printf '"suppress_fail_groups_keying_agent_root_scope_global":%s,"suppress_fail":%s}\n' "$S_FAIL_G" "$S_FAIL_S"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@ -208,7 +212,7 @@ printf ' 类: kind<>'"'"'failure'"'"'(**空真**)=%s;真判据(permiss
|
||||
printf ' failure 边界: %%failure%% 子串=%s;三前缀(无 homeagent)=%s;homeagent:failure: =%s(差 %s)\n' \
|
||||
"$SUBSTR_N" "$PREFIX_N" "$HOMEAGENT" "$((SUBSTR_N - PREFIX_N))"
|
||||
printf ' 测试残留=%s(其中未绑定=%s)\n' "$RESIDUE" "$RESIDUE_UNBOUND"
|
||||
printf ' 抑制数: 完整 parent 链=%s 组/%s 封;只沿 failure 链=%s 组/%s 封\n' \
|
||||
printf ' 抑制数 [keying=(agent,根) scope=全局]: 完整 parent 链=%s 组/%s 封;只沿 failure 链=%s 组/%s 封\n' \
|
||||
"$S_FULL_G" "$S_FULL_S" "$S_FAIL_G" "$S_FAIL_S"
|
||||
printf ' 根计算触到 LOOP/CAP 的次数: 完整链=%s;failure 链=%s\n' "$L_FULL" "$L_FAIL"
|
||||
|
||||
@ -246,10 +250,27 @@ chk_true "完整链口径的抑制数 **大于** failure 链口径" \
|
||||
chk_eq "完整 parent 链的根计算**未触上限**(无环/无超深)" "$L_FULL" "0"
|
||||
chk_eq "failure 链的根计算**未触上限**" "$L_FAIL" "0"
|
||||
# ★ 变异③的补丁: 占位行数本身就是"幂等键是否泄漏"的读数据 —— 光有 total=bound+place 不够
|
||||
# (③ 把一行置 NULL,两边同时变,恒等式仍成立 ⇒ 存活)。真正该断言的是**占位行 <= 1**:
|
||||
# 正常路径下 Claim 后立刻 Bind/Release,占位是**瞬时**的;稳定 >1 意味着有键没退。
|
||||
chk_true "占位行 <= 1(>1 即"占位后没退键"的泄漏信号)" \
|
||||
"$([ "$PLACE" -le 1 ] && echo 1 || echo 0)"
|
||||
# (③ 把一行置 NULL,两边同时变,恒等式仍成立 ⇒ 存活)。
|
||||
#
|
||||
# ⚠️ 但**不能断言 `占位行 <= 1`**(pi 2026-09-25 实测,我复跑确认):
|
||||
# `ClaimRelay`(INSERT 占位)与 `BindRelayMail`(UPDATE 绑定)**不在一个事务里**,
|
||||
# 所以"同时有 2 个请求在飞"的正常形态就是 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。
|
||||
# 实测(/tmp/pl/t.db:清掉化石 + 插 2 个合法在飞占位):
|
||||
# PLACE=2 ⇒ 旧判据 rc=1,报 [FAIL] 占位行 <= 1 —— **把健康并发判成泄漏**
|
||||
# ⇒ 阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。
|
||||
#
|
||||
# ⇒ 与泄漏正交的判别量是**行的年龄**:正常占位行只活"一个请求的时长",泄漏行活到**下一次请求之后**。
|
||||
# 年龄与并发度无关(并发再大,每行寿命仍是一轮请求),也与"此刻是否恰好有请求"无关。
|
||||
# 阈值取 10 分钟:远大于任何单个请求(毫秒~秒级,且 HTTP 有超时),
|
||||
# 又远小于真实泄漏的尺度(那 1 行活了 12.7 天)。
|
||||
# ★ 断言的是**上界**而不是"是不是 0"—— 因为 0 是"恰好没请求在飞",不是健康的条件。
|
||||
IDLE_PLACE=$(q "select count(*) from relayed_mails where mail_id is null and (julianday('now') - julianday(created_at)) * 1440 > 10;")
|
||||
IDLE_AGE=$(q "select cast(max((julianday('now') - julianday(created_at)) * 1440) as int) from relayed_mails where mail_id is null;")
|
||||
printf ' 占位行年龄: 最老 = %s 分钟;其中 >10 分钟(疑似泄漏)= %s\n' "${IDLE_AGE:-0}" "$IDLE_PLACE"
|
||||
# ★ 只打印不判红当且仅当**没有任何占位行**:那种情况下 IDLE_AGE 为 NULL/空,
|
||||
# "没有样本"不等于"样本健康",不能拿它当绿(本文件反复记的假绿形状)。
|
||||
chk_true "没有**超过 10 分钟**的占位行(年龄与并发度正交;阈值 1 会把 2 个在飞判成泄漏)" \
|
||||
"$([ "$IDLE_PLACE" -eq 0 ] && echo 1 || echo 0)"
|
||||
# ★ 变异⑤的补丁: 上面那些都是**关系**断言(A=B、A<B),而把一行 permission 的键改成
|
||||
# failure 形状时,substr 与 prefix **同增** ⇒ 所有关系仍成立 ⇒ ⑤ 存活。
|
||||
# 关系断言测不出"**语义**被搬动",只有**锚定到写路径**的不变量能测。
|
||||
|
||||
@ -436,6 +436,32 @@ case "$_scan2" in
|
||||
esac
|
||||
unset _scan _scan2 2>/dev/null || true
|
||||
|
||||
# 部署后的**同一次**校验:装上去的那份二进制就是当前 HEAD 建的。
|
||||
#
|
||||
# ★ 为什么必须有这一条(pi 2026-09-25 提出,我这边已实现为 check-deploy-drift 判据 ⑤b):
|
||||
# §7 上面那几项(服务 active / /health 200 / 无 panic / SSE 重连)**全都与代码版本无关** ——
|
||||
# 用一个**旧二进制**跑这套清单会**全绿**。2026-09-25 实证:线上跑的是 09-19 的二进制,
|
||||
# 而 §7 当时全过、`/health` 200 ⇒ 没有任何一项能说出"装的不是当前代码"。
|
||||
# 当时唯一的发现手段是人手动跑 `go version -m` —— 一个**不在任何门禁里**的动作。
|
||||
# ⇒ 这里把那个动作自动化:`vcs.revision` 是 Go 以明文编进二进制的(默认 -buildvcs=auto)。
|
||||
#
|
||||
# 三态(与 ⑤b 同口径,别改成两态):
|
||||
# · 相等 ⇒ 绿
|
||||
# · 不等 ⇒ **红**(就地回滚)
|
||||
# · 取不到任一侧 ⇒ 不判(`-buildvcs=false`/老构建/非 git 构建是合法的;判红会恒红)
|
||||
# 不判时**必须说出来**,否则读者分不清"比过且相同"与"根本没比"。
|
||||
_binrev="$(go version -m "$TARGET" 2>/dev/null | sed -n 's/.*vcs\.revision=//p' | head -1)"
|
||||
_headrev="$(git -C "$REPO" rev-parse HEAD 2>/dev/null)"
|
||||
if [ -z "$_binrev" ] || [ -z "$_headrev" ]; then
|
||||
warn "无法比对二进制版本(二进制 revision='${_binrev}',HEAD='${_headrev}')—— **不判**,不等于一致"
|
||||
elif [ "$_binrev" = "$_headrev" ]; then
|
||||
ok "已装二进制 = 当前 HEAD(${_binrev%"${_binrev#????????}"})"
|
||||
else
|
||||
bad "已装二进制是 ${_binrev%"${_binrev#????????}"},HEAD 是 ${_headrev%"${_headrev#????????}"} —— 装的不是当前代码"
|
||||
CHECK_FAIL=$((CHECK_FAIL+1))
|
||||
fi
|
||||
unset _binrev _headrev 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo " 仍需人工确认(脚本无法代替):"
|
||||
echo " [ ] 真实发一封邮件端到端跑通(不是只看进程起来了)"
|
||||
|
||||
Reference in New Issue
Block a user