diff --git a/docs/tests/report-test823.txt b/docs/tests/report-test823.txt new file mode 100644 index 00000000..2ec740b5 --- /dev/null +++ b/docs/tests/report-test823.txt @@ -0,0 +1,36 @@ +# report-test823 — L1 concurrency cap gate + +Date: 2026-08-13 (Asia/Shanghai) +Base: 034f00647d42d38d5086d7fc057eb7824a441791 +Source: 76c12e98379b67eb074a2a42e2a170e6aa94db1f +run.sh blob: 7c4e3ebb4ecf13710d166d9a0d3f26eb43d8d9d1 (容器内就地重算并比对,不只验 SHA 格式) +Run: docker build --build-arg SOURCE_COMMIT=$SRC --build-arg RUNSH_BLOB=$BLOB … && docker run --rm --network none … +Exit: 0 + +## 本次运行 +``` +- cap=2 峰值=2 生效值=2 告警=0 + ok cap2: 生效上限 (= 2) + ok cap2: 峰值 2 ≤ 2 +- 非法值 two 峰值=8 生效值=8 告警=1 + ok bad_warn: 必须告警 (= 1) + ok bad_eff: 退回默认(不是静默不限) (= 8) +- 前导零 08 峰值=8 生效值=8 告警=0 + ok octal_eff: 按十进制解释,不是八进制报错/不限 (= 8) +- 0(不限) 峰值=13 生效值=0 告警=0 + ok zero_eff: 0 保留为「不限」的逃生口 (= 0) + ok zero_conc: 不限时峰值 13 > cap2 的 2,确实放开了并发 +failures=0 +RESULT: PASS +- cap=2 峰值=2 生效值=2 告警=0 +``` + +## 见证红(三种) +``` +1. 去掉 qa.sh 的取值校验段: + FAIL bad_warn: 必须告警 —— 期望 1,实测 0 + FAIL bad_eff: 退回默认(不是静默不限) —— 期望 8,实测 ? + FAIL octal_eff: 按十进制解释,不是八进制报错/不限 —— 期望 8,实测 08 +2. 传错的 run.sh blob 哈希 → FAIL: 镜像里的 run.sh 与 SOURCE_COMMIT 声称的不是同一份 (exit 1) +3. blob 参数对但文件被篡改 → 同上 (exit 1) +``` diff --git a/scripts/qa.sh b/scripts/qa.sh index e7135473..fbc53232 100755 --- a/scripts/qa.sh +++ b/scripts/qa.sh @@ -57,6 +57,9 @@ L0_TESTS=( # 它的 CI 归属是会安装依赖的层级;本地跑法见该文件头注释的门禁命令。 ) L1_TESTS=( + # 这道闸门自己的回归。放在最前:它跑的是本脚本,若闸门坏了应当最先暴露。 + # (注册这一步不是可选的 —— 一个没被任何东西调用的套件等于不存在。) + "test823-l1-concurrency-cap" "qa-cli-01-hub-start" "qa-cli-02-network-create" "qa-dash-07-auth-boundary" @@ -159,15 +162,35 @@ if [[ $RUN_L1 -eq 1 ]]; then for t in "${L1_TESTS[@]}"; do # Build (cached if recent) note "build $t" + # 🔴 原来这里是一串逐套件的 elif。那正是本 PR 撞红的成因: + # test823 被加进 L1_TESTS,却没人记得在这里也加一条,于是 + # TEST823_SOURCE_COMMIT 是空串,门 fail-closed: + # FAIL: TEST823_SOURCE_COMMIT 必须是一个完整的小写 SHA(收到 '') + # 「注册了套件但忘了在供给侧登记」会一直复发,所以改成按套件名推导: + # testNNN-... → --build-arg TESTNNN_SOURCE_COMMIT= + # qa-*-... 形态不匹配,和以前一样不传(它们的门不要这个变量)。 build_args="" - if [[ "$t" == "test686-rest-shape-golden" ]]; then - build_args="--build-arg TEST686_SOURCE_COMMIT=$(git rev-parse HEAD)" - elif [[ "$t" == "test765-batch-runtime-gate" ]]; then - build_args="--build-arg TEST765_SOURCE_COMMIT=$(git rev-parse HEAD)" - elif [[ "$t" == "test766-bunx-preflight" ]]; then - build_args="--build-arg TEST766_SOURCE_COMMIT=$(git rev-parse HEAD)" - elif [[ "$t" == "test746-setup-bun-pin" ]]; then - build_args="--build-arg TEST746_SOURCE_COMMIT=$(git rev-parse HEAD)" + if [[ "$t" =~ ^test([0-9]+)- ]]; then + # 🔴 仓里并存两套 build-arg 命名,必须都供给,否则名字对不上就是空串: + # 旧:test686/765/766/746 的 Dockerfile 收 TESTNNN_SOURCE_COMMIT + # 新:test823(以及 test798/test831)的 Dockerfile 收 SOURCE_COMMIT + RUNSH_BLOB + # 我上一版只按套件名推导出 TESTNNN_SOURCE_COMMIT,test823 收的是 SOURCE_COMMIT, + # 于是照旧红在 "TEST823_SOURCE_COMMIT 收到 ''"。 + # 未被 Dockerfile 声明的 build-arg 只会产生一条警告,不影响构建,所以两套都传。 + # 🔴 git 调用必须是非致命的。qa.sh 头部是 `set -euo pipefail`,而 test823 + # 会在一个**只装了 bash/coreutils/procps、没有 git** 的容器里重放这个脚本 + # (它桩了 docker 和 npm,但没桩 git)。上一版我直接写 $(git rev-parse HEAD), + # 容器里 git 不存在 → 127 → set -e 当场中断 → docker 桩一次都没被调用 → + # 峰值恒为 0 → 闸门自己的回归失败。这是我改出来的回归,不是被测代码的问题。 + _qa_sha="$(git rev-parse HEAD 2>/dev/null || true)" + if [ -n "$_qa_sha" ]; then + build_args="--build-arg TEST${BASH_REMATCH[1]}_SOURCE_COMMIT=$_qa_sha --build-arg SOURCE_COMMIT=$_qa_sha" + # blob 绑定:把 SOURCE_COMMIT 钉到被测的那份 run.sh 字节上(git blob object id) + if [ -f "tests/$t/run.sh" ]; then + _qa_blob="$(git rev-parse "HEAD:tests/$t/run.sh" 2>/dev/null || true)" + [ -n "$_qa_blob" ] && build_args="$build_args --build-arg RUNSH_BLOB=$_qa_blob" + fi + fi fi if ! dockerrun "docker build -q $build_args -t anet-$t -f tests/$t/Dockerfile ." >/tmp/qa-l1-$t-build.log 2>&1; then fail "L1 $t — build failed, see /tmp/qa-l1-$t-build.log" diff --git a/tests/test823-l1-concurrency-cap/Dockerfile b/tests/test823-l1-concurrency-cap/Dockerfile new file mode 100644 index 00000000..5de27696 --- /dev/null +++ b/tests/test823-l1-concurrency-cap/Dockerfile @@ -0,0 +1,23 @@ +ARG SOURCE_COMMIT +FROM node:22-bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash ca-certificates coreutils procps \ + && rm -rf /var/lib/apt/lists/* + +ARG SOURCE_COMMIT +ARG RUNSH_BLOB +ENV TEST823_SOURCE_COMMIT=${SOURCE_COMMIT} +# 把 run.sh 在 SOURCE_COMMIT 下的 git blob 哈希带进来,让容器内能验证 +# "报告里的 SHA 确实对应镜像里被测的字节",而不是只验 SHA 的格式。 +ENV TEST823_RUNSH_BLOB=${RUNSH_BLOB} + +WORKDIR /workspace +COPY scripts/qa.sh /workspace/scripts/qa.sh +COPY tests/test823-l1-concurrency-cap/run.sh /workspace/run.sh + +# 非 root:与本仓其它套件一致 +RUN chown -R node:node /workspace +USER node + +ENTRYPOINT ["bash", "/workspace/run.sh"] diff --git a/tests/test823-l1-concurrency-cap/run.sh b/tests/test823-l1-concurrency-cap/run.sh new file mode 100755 index 00000000..7c4e3ebb --- /dev/null +++ b/tests/test823-l1-concurrency-cap/run.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# test823 — scripts/qa.sh 的 L1 并发上限闸门 +# +# 这道闸门的失效方向是 fail-open:QA_L1_MAX_PAR 拿到非数字时,bash 在算术 +# 上下文里把它当 0,而 0 的语义恰好是「不限」—— 于是一个笔误会静默恢复 +# 无上限运行。无上限时实测宿主 load1 顶到 58(8 核,同时跑着生产 hub、 +# dashboard 与约 200 个 session),所以这条不是形式主义。 +# +# 🔴 本套件跑的是**真的 scripts/qa.sh**,不是逻辑副本。 +# 做法:把 `docker` 换成 PATH 上的桩。qa.sh 的 dockerrun() 是 +# `bash -c "$*"`,所以它会解析到桩;真实的闸门代码原样执行。 +# 在副本上测只能证明副本自洽 —— 那正是本仓反复栽过的坑。 +# +# 峰值用**事件流**算,不用采样:每次桩调用写下精确的 START / END 纳秒 +# 时间戳,事后排序求最大重叠。采样会漏掉峰值,事件流不会。 +set -uo pipefail + +ROOT=/workspace +SRC=${TEST823_SOURCE_COMMIT:-} +[[ "$SRC" =~ ^[0-9a-f]{40}$ ]] || { echo "FAIL: TEST823_SOURCE_COMMIT 必须是一个完整的小写 SHA(收到 '${SRC}')" >&2; exit 1; } + +# 🔴 光验格式不够 —— 任何 40 位十六进制都能通过,而报告里那个 SHA 可能 +# 根本不含镜像里被测的文件。这正是 #801 上那条 P1 说的病,我建这个 +# 套件时原样犯了一遍。 +# 做法:构建时把 run.sh 在该 commit 下的 git blob 哈希作为 build-arg +# 传进来,这里就地重算镜像内文件的 blob 哈希并比对。git blob 哈希 = +# sha1("blob \0" + 内容),不需要容器里装 git。 +EXPECT=${TEST823_RUNSH_BLOB:-} +[[ "$EXPECT" =~ ^[0-9a-f]{40}$ ]] || { echo "FAIL: TEST823_RUNSH_BLOB 缺失或格式不对 —— 无法把 SOURCE_COMMIT 绑到被测字节" >&2; exit 1; } +self=/workspace/run.sh +actual=$( { printf 'blob %d\0' "$(wc -c < "$self")"; cat "$self"; } | sha1sum | cut -d" " -f1 ) +if [[ "$actual" != "$EXPECT" ]]; then + echo "FAIL: 镜像里的 run.sh 与 SOURCE_COMMIT=$SRC 声称的不是同一份" >&2 + echo " 期望 blob $EXPECT,实际 $actual" >&2 + exit 1 +fi + +BIN=/tmp/t823-bin +EV=/tmp/t823-events +mkdir -p "$BIN" + +# ── docker 桩 ──────────────────────────────────────────────────────────── +# build 是同步的,run 是后台的 —— 只有 run 会重叠。两者都记事件, +# 这样如果哪天 build 也被后台化,峰值会立刻反映出来。 +cat > "$BIN/docker" <<'STUB' +#!/usr/bin/env bash +# build 是同步的、run 是后台的 —— 只有 run 会重叠。若两者同样耗时, +# run 之间几乎不重叠,峰值恒为 1,高上限下断言就失去分辨力(第一版如此)。 +# 所以 build 尽量快,run 拉长,让并发真正显现出来。 +# 只有真正的 `docker run` 才记事件。任何其它子命令(build/ps/rmi…) +# 都不该计入并发峰值 —— 否则峰值会被无关调用抬高,断言就不再是在测闸门。 +[ "${1:-}" = "run" ] || exit 0 +printf 'S %s %s\n' "$(date +%s%N)" "$$" >> "$T823_EV" +sleep 1.2 +printf 'E %s %s\n' "$(date +%s%N)" "$$" >> "$T823_EV" +exit 0 +STUB +chmod +x "$BIN/docker" + +# npm 也要桩:qa.sh 会跑 `npm view … dist-tags.preview` 做 registry 快照。 +# 容器是 --network none,真 npm 会一直等 DNS/连接超时,而不是快速失败 —— +# 第一版就是这么跑成超时的。桩掉它,让被测的闸门成为唯一的耗时来源。 +cat > "$BIN/npm" <<'NPMSTUB' +#!/usr/bin/env bash +echo "0.0.0-stub" +exit 0 +NPMSTUB +chmod +x "$BIN/npm" + +export PATH="$BIN:$PATH" +export T823_EV="$EV" + +peak() { # 从事件流算最大重叠 + sort -k2,2n "$1" | awk ' + $1=="S" { c++; if (c>m) m=c } + $1=="E" { c-- } + END { print m+0 }' +} + +nproc_val=$(nproc 2>/dev/null || echo 4) +fails=0 +report=/tmp/report-test823.txt +: > "$report" + +say() { echo "$*" | tee -a "$report"; } + +say "# test823 — L1 concurrency cap gate" +say "source_commit=$SRC" +say "nproc=$nproc_val" +say "" + +run_case() { # $1=用例名 $2=QA_L1_MAX_PAR 取值(空=不设) + local name=$1 val=${2-} + : > "$EV" + local out=/tmp/t823-$name.log + if [[ -n "${val:-}" || "${2+set}" == "set" ]]; then + QA_L1_MAX_PAR="$val" bash "$ROOT/scripts/qa.sh" --l1 > "$out" 2>&1 || true + else + bash "$ROOT/scripts/qa.sh" --l1 > "$out" 2>&1 || true + fi + local p; p=$(peak "$EV") + # 只取 `= ` 之后那个数。原来用 grep -oE '[0-9]+' 会先命中 "L1" 里的 1 —— + # 判据没在已知输入上校准过,于是四个用例全部报 1。 + local eff; eff=$(sed -n 's/.*L1 并发上限 = \([0-9][0-9]*\).*/\1/p' "$out" | head -1) + [[ -n "$eff" ]] || eff="?" + local warned=0; grep -q '不是非负整数' "$out" && warned=1 + echo "$p|$eff|$warned" +} + +check() { # $1=用例 $2=实测 $3=期望 $4=说明 + if [[ "$2" == "$3" ]]; then say " ok $1: $4 (= $2)" + else say " FAIL $1: $4 —— 期望 $3,实测 $2"; fails=$((fails+1)); fi +} + +say "## 用例" + +IFS='|' read -r p eff warned <<< "$(run_case cap2 2)" +say "- cap=2 峰值=$p 生效值=$eff 告警=$warned" +check cap2 "$eff" 2 "生效上限" +[[ "$p" -le 2 && "$p" -ge 1 ]] && say " ok cap2: 峰值 $p ≤ 2" || { say " FAIL cap2: 峰值 $p 超过上限 2"; fails=$((fails+1)); } + +IFS='|' read -r p eff warned <<< "$(run_case bad two)" +say "- 非法值 two 峰值=$p 生效值=$eff 告警=$warned" +check bad_warn "$warned" 1 "必须告警" +check bad_eff "$eff" "$nproc_val" "退回默认(不是静默不限)" + +IFS='|' read -r p eff warned <<< "$(run_case octal 08)" +say "- 前导零 08 峰值=$p 生效值=$eff 告警=$warned" +check octal_eff "$eff" 8 "按十进制解释,不是八进制报错/不限" + +IFS='|' read -r p eff warned <<< "$(run_case zero 0)" +say "- 0(不限) 峰值=$p 生效值=$eff 告警=$warned" +check zero_eff "$eff" 0 "0 保留为「不限」的逃生口" +# 只断言"生效值是 0"不够 —— 那只证明它被这么解析,没证明它真的放开了并发。 +# 拿 cap=2 的峰值当对照:不限时必须明显更高。 +if [[ "$p" -gt 2 ]]; then say " ok zero_conc: 不限时峰值 $p > cap2 的 2,确实放开了并发" +else say " FAIL zero_conc: 不限时峰值只有 $p,与 cap=2 无区别 —— 「不限」没有被验证"; fails=$((fails+1)); fi + +say "" +say "failures=$fails" +if [[ "$fails" -eq 0 ]]; then say "RESULT: PASS"; else say "RESULT: FAIL"; fi +cat "$report" +[[ "$fails" -eq 0 ]]