Skip to content

Commit bdef95c

Browse files
ShanireZclaude
andcommitted
stats 那条间歇红坐实了:判据抢在缓存落地之前读 —— 改成轮询,而硬编码 0 那一族照样红
`tests/stats/run.sh` [3/5] 的 `cache.entries` 那条,在完整门禁里三次红过一次、单跑从没红过。 ## 根因(读源码 + 注入坐实,⛔ 不是推理) 产品在**响应体写完之后**才存缓存:`crates/fulcrum-server/src/lib.rs` 里 `write_response_body(Bytes::new(), true)` 在前、`store_if_allowed` 在后 —— 这是有意的,缓存写入不该挂在客户端的延迟路径上。上游夹具带 `Content-Length` ⇒ curl 在最后一个体字节到达时就退出了,而判据紧接着走 admin socket 读 `/stats`, 中间没有任何等待 ⇒ 机器满载时读到 `entries=0`。 ★ 在那两行之间注入 300 ms ⇒ 旧判据 **100% 红**,报文与门禁里那次逐字相同; 还原(sha256 与字节备份逐字相同)⇒ 回绿。 佐证:`tests/cache/run.sh` 判缓存一律靠再发一次请求看 `X-Fulcrum-Cache: HIT`, 天然隔着一整个客户端往返 ⇒ 全仓只有这一格在响应刚回来就走旁路读缓存状态。 ## 修法:在原断言之前轮询,⛔ 产品一行不改 最多 50 次 × 0.1 秒;循环里只取数不判定(用 `capture`,不用 `capture_ok`, 否则每轮没等到都记一笔失败);判定仍是原来那几行断言,一个字节没动。 ⚠ 放弃的是「存入变慢、但慢不过 5 秒」那一族 —— 那从来不是这条判据的目标; 「把 cache 硬编码成 `{entries:0}`」那一族照样判红。 ## 判据(四个方向全部本机实测;注入 → 跑 → 还原串成一条命令,挂 trap 保证还原) · NC-C 存入前延迟 300 ms ⇒ 新判据 `GATE_RC=0`,**第 4 次读**才看到条目落地(轮询真的在等) · NC-A 永不存入(`captured.filter(|_| black_box(false))`)⇒ `GATE_RC=1`,读满 50 次仍未落地, 报文「期望「1」实际「0」」与旧判据逐字相同(判别力没丢) · 干净基线 ⇒ `GATE_RC=0`,第 1 次就读到 · 四趟日志里都有新鲜的 `Compiling fulcrum-server`;终态 `lib.rs` sha256 与备份逐字相同,源码零注入残留 · `LINT_ONLY=1`:shellcheck 53 个脚本全过 · 完整门禁 `bash tests/m0/docker-run.sh`:`GATE_RC=0`(6438 行,0 个 `✗`); 单测 28 个二进制 884 条全绿,**我们自己的 crates/ 0 条 ignored**(零跳过); vendor 那 2 条失败与 10 条 ignored 与官方原版 0.8.1 逐项相同; stats 那一格在完整门禁的负载下第 1 次就读到 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2c9fbef commit bdef95c

1 file changed

Lines changed: 28 additions & 0 deletions

File tree

tests/stats/run.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,34 @@ capture_ok "打一条会被缓存的请求" curl -sS -o /dev/null -w '%{http_cod
370370
--max-time 5 -H "Host: s.example" "http://$HOST:$S_PORT/cacheable"
371371
eq "请求走通(回源到上游)" 200 "$CAPTURE_OUT"
372372

373+
# ⚠ ⚠ ★ **先轮询,⛔ 不能 curl 一回来就死读一次**(2026-09-10 坐实;此前完整门禁里红过):
374+
# 产品在**响应体写完之后**才把条目存进缓存 —— `crates/fulcrum-server/src/lib.rs` 里
375+
# `write_response_body(…, true)` 在前、`store_if_allowed` 在后,而这是**有意的**:
376+
# 缓存写入不该挂在客户端的延迟路径上。上游带 `Content-Length` ⇒ curl 在最后一个体字节
377+
# 到达时就退出了,此后服务端还要走完「读到 EOF → 压缩收尾 → 写空尾块 → 存」。
378+
# ⇒ 一次死读在机器满载时会读到 0(完整门禁里红过;单跑 `STATS_ONLY` 从没红过)。
379+
# ★ 在那两行之间注入 300 ms 延迟 ⇒ 旧写法 100% 红,报文逐字相同。
380+
# ★ 轮询**不削弱**这条判据要抓的东西:一个把 cache 硬编码成 `{entries:0}` 的实现永远
381+
# 等不到 1 ⇒ 超时后下面那条照原样红。⚠ 它放弃的是「存入变慢、但慢不过 5 秒」那一族,
382+
# 而那从来不是这条判据的目标。
383+
# ⛔ 循环里只取数、不判定(用 `capture` 不用 `capture_ok`,否则每轮没等到都记一笔失败);
384+
# 判定仍是下面那几行原样的断言。
385+
C_TRIES=0
386+
while [ "$C_TRIES" -lt 50 ]; do
387+
capture admin_get /stats
388+
if [ "$CAPTURE_OUT" = 200 ]; then
389+
capture sc cache_entries "$WORK/admin.out"
390+
if [ "$CAPTURE_RC" -eq 0 ] && [ "$CAPTURE_OUT" = 1 ]; then break; fi
391+
fi
392+
sleep 0.1
393+
C_TRIES=$((C_TRIES + 1))
394+
done
395+
if [ "$C_TRIES" -lt 50 ]; then
396+
echo " · 第 $((C_TRIES + 1)) 次读 /stats 看到条目落地(上限 50 次 ≈ 5 秒)"
397+
else
398+
echo " · 读了 50 次(≈ 5 秒)条目仍未落地 —— 下面那条会照原样判红"
399+
fi
400+
373401
capture_ok "GET /stats(缓存之后)" admin_get /stats
374402
eq "GET /stats(缓存之后)" 200 "$CAPTURE_OUT"
375403
cp "$WORK/admin.out" "$WORK/stats2.json"

0 commit comments

Comments
 (0)