Skip to content

Commit 263e5eb

Browse files
fix(leaderboard): Copilot CR 反馈
- fetch 加 AbortController 15s 超时:防止后端 TCP 建立后不返回时 build 无限挂起 (Vercel build 单步通常 <5min,留 15s 给后端 Caffeine 命中即毫秒,未命中也秒级) - 降级写空榜单的 mkdir + writeFile 放同一 try/catch:任一步失败 exit 1 fail-fast 避免文件不存在但 exit 0 让 deploy "看似正常",后续 Next import 抛 ENOENT 更难定位
1 parent 8ade968 commit 263e5eb

1 file changed

Lines changed: 32 additions & 9 deletions

File tree

scripts/generate-leaderboard.mjs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ async function ensureParentDir(filePath) {
8282
await fs.mkdir(dir, { recursive: true });
8383
}
8484

85+
// 后端响应超时上限:Vercel build 单步通常 5min 内,给后端 15s 足够
86+
// (Caffeine 命中是毫秒级,未命中走 JDBC 全表扫描也就秒级)。超时即降级。
87+
const FETCH_TIMEOUT_MS = 15_000;
88+
8589
/**
86-
* 拉后端聚合数据。任何错误都返回 null,让调用方决定降级策略
90+
* 拉后端聚合数据。任何错误(含超时)都返回 null,让调用方决定降级策略
8791
* (生成空榜单放行 build vs. 整个失败)。
8892
*
8993
* 后端 ApiResponse 形如 { success, message, data },data 是 LeaderboardEntryDto[]。
@@ -92,12 +96,16 @@ async function fetchAggregatedFromBackend() {
9296
console.log(
9397
`[generate-leaderboard] 拉聚合数据:${LEADERBOARD_API_URL} | Fetching aggregated contributions from backend...`,
9498
);
99+
// AbortController 超时:防止后端 TCP 建立后不返回时 build 无限挂起
100+
const controller = new AbortController();
101+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
95102
try {
96103
const res = await fetch(LEADERBOARD_API_URL, {
97104
headers: {
98105
accept: "application/json",
99106
"user-agent": "InvolutionHell-build/1.0 (generate-leaderboard.mjs)",
100107
},
108+
signal: controller.signal,
101109
});
102110
if (!res.ok) {
103111
console.error(
@@ -115,11 +123,19 @@ async function fetchAggregatedFromBackend() {
115123
}
116124
return data;
117125
} catch (err) {
118-
console.error(
119-
"[generate-leaderboard] 调用后端失败:",
120-
err instanceof Error ? err.message : err,
121-
);
126+
if (err && err.name === "AbortError") {
127+
console.error(
128+
`[generate-leaderboard] 后端响应超时(${FETCH_TIMEOUT_MS}ms),降级为空榜单`,
129+
);
130+
} else {
131+
console.error(
132+
"[generate-leaderboard] 调用后端失败:",
133+
err instanceof Error ? err.message : err,
134+
);
135+
}
122136
return null;
137+
} finally {
138+
clearTimeout(timeoutId);
123139
}
124140
}
125141

@@ -132,13 +148,20 @@ async function main() {
132148
console.error(
133149
"[generate-leaderboard] 后端不可用,写入空榜单以放行构建。 | Backend unreachable, writing empty leaderboard to unblock build.",
134150
);
135-
await ensureParentDir(outputAbs);
151+
// mkdir + writeFile 必须放同一个 try:任一步失败都意味着 generated/site-leaderboard.json
152+
// 不存在,后续 Next 端 import 会抛更难定位的 ENOENT。这种情况 build 必须 fail-fast,
153+
// 不能 exit 0 让"看起来一切正常"的 deploy 把站点搞挂。
136154
try {
155+
await ensureParentDir(outputAbs);
137156
await fs.writeFile(outputAbs, "[]", "utf-8");
138-
} catch {
139-
// ignore
157+
process.exit(0);
158+
} catch (err) {
159+
console.error(
160+
"[generate-leaderboard] 写入空榜单失败,无法继续放行构建:",
161+
err instanceof Error ? err.stack || err.message : err,
162+
);
163+
process.exit(1);
140164
}
141-
process.exit(0);
142165
}
143166

144167
// 构建 docId → {title, url} 映射,从 .source/index.ts 提取(Fumadocs 生成的 manifest)

0 commit comments

Comments
 (0)