Skip to content

Commit 1ad2c74

Browse files
authored
ci(mirror): 每资源计时 + 2m 上传上限(超时 warn 并跳过) (#219)
* ci(mirror): 每资源计时 + 2m 上传上限(超时 warn 并跳过) v0.0.94 发布时 publish-ecosystem 被 30min job timeout 杀掉,日志里看不出是 哪个资产在慢 —— 事后查是 gtc 跨境传 linux-x86_64(8.4MB)/linux-aarch64 (30.5MB)两个大件卡住,最后靠本地 gtc 手工补齐。 - **每资源计时**:上传返回即打印 `<github|gitcode> <资产> (<大小>) uploaded in Ns`; 已 serving 的仍先 probe → 打印 skipping,不重传(原有行为,补上 host 标签)。 - **2m 上限**(`MIRROR_UPLOAD_TIMEOUT`,默认 120s):超时打印 WARN + **放弃该资产**, 后续轮次不再重试。 - 每条 mirror leg 结束打印耗时;失败时 hint 出手工补传命令。 **为何这个 cap 与 v0.0.90 事故不同**:那次 `timeout 300` 杀掉慢但**在推进**的上传 后**重传**,每次从字节零开始,永不收敛,把 20min job 预算拖垮。本 cap 的安全性来自 **超时即放弃、绝不重试**;完整性 gate 仍是唯一 pass/fail,漏传会响亮报错而不是 静默发半个镜像。脚本顶部的血泪注释已按此改写。 验证: - 真实 0.0.94 镜像干跑 → 16 资产两端全 200、已存在全部 skip、206s 通过。 - stub 双分支(PATH 上的真可执行文件,非 shell function —— timeout 对函数无效): 快传 → `github big.tar.gz (2.9MB) uploaded in 1s`; 慢传 → `WARN: gitcode ... exceeded the 2s cap after 2s — skipping`。 * ci(mirror): 单资产 cap 3min + 镜像段 10min 硬顶 按 review 定档: - `MIRROR_UPLOAD_TIMEOUT` 120 → **180s**。mcpp 最大资产约 30MB(单包不超 100M), 3 分钟还没传完不是「慢」是「卡住」,不值得继续占 CI —— 跳过并由本地补传 (gate 会打印确切的补传命令)。 - Mirror 步骤加 **`timeout-minutes: 10`** 硬顶。健康的 run 远在此之内;10min 是 兜底,防止某一端病态时烧光 job 预算(v0.0.94 就是 30min 挂死且零可见性)。
1 parent 450c24b commit 1ad2c74

2 files changed

Lines changed: 83 additions & 15 deletions

File tree

.github/tools/mirror_res.sh

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,58 @@ read -r -a ASSETS <<< "${ASSETS:-$DEFAULT_ASSETS}"
4242

4343
info() { echo "[mirror] $*"; }
4444

45+
# Per-asset upload cap. Exceeding it WARNs and abandons that asset (no retry,
46+
# no delete) so one slow cross-border PUT can't eat the whole job budget — the
47+
# v0.0.94 release lost `publish-ecosystem` to a 30min job timeout with zero
48+
# per-asset visibility, and the two biggest linux tarballs had to be pushed by
49+
# hand afterwards.
50+
#
51+
# CAUTION (v0.0.90 postmortem, still binding): a cap that kills a
52+
# slow-but-PROGRESSING upload and then RETRIES it is strictly worse than no cap
53+
# — every restart resumes from byte zero and the mirror never converges. This
54+
# cap is safe only because a capped asset is SKIPPED, never re-uploaded. The
55+
# completeness gate at the bottom is still the pass/fail, so a skipped asset
56+
# fails the release loudly instead of silently shipping a half mirror.
57+
#
58+
# Sizing: mcpp's largest asset is ~30MB (no package exceeds 100MB). 180s is a
59+
# generous ceiling for that — an upload still running at 3min is not "slow", it
60+
# is stuck, and the right move is to stop paying CI for it and push that one
61+
# asset by hand (the gate below prints the exact command).
62+
: "${MIRROR_UPLOAD_TIMEOUT:=180}"
63+
4564
DL="$(mktemp -d)"; trap 'rm -rf "$DL"' EXIT
4665

66+
human_size() { # path → e.g. 31.9MB
67+
local b; b=$(stat -c %s "$1" 2>/dev/null || stat -f %z "$1" 2>/dev/null || echo 0)
68+
awk -v b="$b" 'BEGIN{ if (b>=1048576) printf "%.1fMB", b/1048576; else if (b>=1024) printf "%.1fKB", b/1024; else printf "%dB", b }'
69+
}
70+
71+
host_label() { [[ "$1" == gh ]] && echo github || echo gitcode; }
72+
73+
# Upload one asset under the cap, timing it. 0 = the command returned within
74+
# the cap (NOT proof it landed — gtc's exit code lies both ways, so the probe /
75+
# gate remains the only source of truth); 1 = the cap fired, asset abandoned.
76+
upload_asset() { # kind(gh|gtc) asset → 0 returned / 1 capped
77+
local kind="$1" a="$2" start elapsed rc=0 sz host
78+
sz=$(human_size "$DL/$a")
79+
host=$(host_label "$kind")
80+
start=$SECONDS
81+
if [[ "$kind" == gh ]]; then
82+
GH_TOKEN="${XLINGS_RES_TOKEN:-}" timeout "$MIRROR_UPLOAD_TIMEOUT" \
83+
gh release upload "$VER" "$DL/$a" -R "$GH_DST" --clobber >/dev/null 2>&1 || rc=$?
84+
else
85+
timeout "$MIRROR_UPLOAD_TIMEOUT" gtc release upload "$GTC_DST" "$DL/$a" --tag "$VER" \
86+
>/dev/null 2>&1 || rc=$?
87+
fi
88+
elapsed=$((SECONDS - start))
89+
if [[ $rc == 124 || $rc == 137 ]]; then
90+
info "WARN: $host $a ($sz) exceeded the ${MIRROR_UPLOAD_TIMEOUT}s cap after ${elapsed}s — skipping (not retried; the verify gate below decides the release)"
91+
return 1
92+
fi
93+
info "$host $a ($sz) uploaded in ${elapsed}s"
94+
return 0
95+
}
96+
4797
info "downloading $SRC_REPO v$VER assets ($PROJ)"
4898
for a in "${ASSETS[@]}"; do
4999
gh release download "v$VER" -R "$SRC_REPO" -D "$DL" -p "$a" 2>/dev/null || { echo "[mirror] FAIL: missing $a in $SRC_REPO v$VER" >&2; exit 1; }
@@ -93,36 +143,43 @@ verify_batch() { # base_url asset... → prints assets still not serving
93143
# Hard-won rules (0.0.86 / 0.0.89 / 0.0.90 postmortems):
94144
# - NEVER delete on a verify timeout — the eager 404→delete loop repeatedly
95145
# deleted GOOD uploads whose propagation was merely slow.
96-
# - NEVER kill a slow-but-progressing upload: the outer `timeout` MUST
97-
# exceed gtc's inner PUT timeout (600s). v0.0.90 wrapped uploads in
98-
# `timeout 300`, so every cross-border PUT >5min was SIGKILLed at 60%%
99-
# and restarted from byte zero — the 20min job ceiling fell to this.
146+
# - NEVER kill a slow-but-progressing upload AND RETRY IT. v0.0.90 wrapped
147+
# uploads in `timeout 300`, so every cross-border PUT >5min was SIGKILLed
148+
# at 60%% and restarted from byte zero — the 20min job ceiling fell to
149+
# this. MIRROR_UPLOAD_TIMEOUT keeps a cap but ABANDONS the asset instead of
150+
# retrying it, which is what makes the cap safe; the gate then fails the
151+
# release loudly rather than thrashing until the job is killed.
100152
# - gtc's exit code lies both ways (obs_callback flakiness); the download
101153
# probe is the only source of truth.
102154
mirror_host() { # kind(gh|gtc) base_url
103155
local kind="$1" base="$2" try a
104156
local pending failed
157+
local -A capped=()
158+
local host_start=$SECONDS
159+
local host; host=$(host_label "$kind")
105160
for try in 1 2 3; do
106161
pending=()
107162
for a in "${ASSETS[@]}"; do
163+
# A capped asset is never re-attempted (see MIRROR_UPLOAD_TIMEOUT).
164+
[[ -n "${capped[$a]:-}" ]] && continue
165+
# Step 1: already serving? then it's mirrored — never re-upload it.
108166
if probe "${base}/${a}"; then
109-
[[ $try == 1 ]] && info "$kind $a already mirrored, skipping"
167+
[[ $try == 1 ]] && info "$host $a already mirrored, skipping"
110168
continue
111169
fi
112-
pending+=("$a")
113-
if [[ "$kind" == gh ]]; then
114-
GH_TOKEN="${XLINGS_RES_TOKEN:-}" timeout 900 \
115-
gh release upload "$VER" "$DL/$a" -R "$GH_DST" --clobber || true
170+
if upload_asset "$kind" "$a"; then
171+
pending+=("$a")
116172
else
117-
timeout 900 gtc release upload "$GTC_DST" "$DL/$a" --tag "$VER" \
118-
>/dev/null 2>&1 || true
173+
capped[$a]=1
119174
fi
120175
done
121-
[[ ${#pending[@]} == 0 ]] && return 0
176+
[[ ${#pending[@]} == 0 ]] && break
122177
failed=$(verify_batch "$base" "${pending[@]}")
123-
[[ -z "$failed" ]] && return 0
124-
echo "[mirror] $kind not serving after patience (try $try): $failed — re-uploading (no delete)"
178+
[[ -z "$failed" ]] && break
179+
info "$host not serving after patience (try $try): $failed — re-uploading (no delete)"
125180
done
181+
((${#capped[@]})) && info "WARN: $host abandoned ${#capped[@]} asset(s) at the ${MIRROR_UPLOAD_TIMEOUT}s cap: ${!capped[*]}"
182+
info "$host mirror leg finished in $((SECONDS - host_start))s"
126183
return 0 # the completeness gate below is the real pass/fail
127184
}
128185

@@ -174,5 +231,9 @@ for host in "${hosts[@]}"; do
174231
[[ "$code" == 200 ]] || { rc=1; echo "[mirror] FAIL: missing/unverified: https://${host}/releases/download/${VER}/${a}" >&2; }
175232
done
176233
done
177-
[[ $rc == 0 ]] && info "all assets mirrored + verified on ${#hosts[@]} host(s)"
234+
if [[ $rc != 0 ]]; then
235+
echo "[mirror] hint: if the asset above was WARNed as capped at ${MIRROR_UPLOAD_TIMEOUT}s, either raise MIRROR_UPLOAD_TIMEOUT for this run or push it by hand:" >&2
236+
echo "[mirror] gh release download v$VER -R $SRC_REPO -p '<asset>' && gtc release upload $GTC_DST '<asset>' --tag $VER" >&2
237+
fi
238+
[[ $rc == 0 ]] && info "all assets mirrored + verified on ${#hosts[@]} host(s) in ${SECONDS}s"
178239
exit $rc

.github/workflows/release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,13 @@ jobs:
725725

726726
- name: Mirror binaries to xlings-res/mcpp (gh + gtc)
727727
if: ${{ env.XLINGS_RES_TOKEN != '' }}
728+
# Hard ceiling for the whole mirror segment. The script caps each asset
729+
# at MIRROR_UPLOAD_TIMEOUT (180s) and abandons anything slower, so a
730+
# healthy run lands well inside this; 10min is the backstop that keeps a
731+
# pathological host from burning the job's budget the way v0.0.94 did
732+
# (30min, killed, zero per-asset visibility). Whatever gets skipped is
733+
# reported by name + size and pushed by hand.
734+
timeout-minutes: 10
728735
env:
729736
GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }}
730737
run: |

0 commit comments

Comments
 (0)