// 通用镜像管理与故障转移 // 每个 source 提供候选镜像列表,运行时记住"当前可用"镜像; // 失败的镜像会被临时拉黑(带过期时间),避免站点恢复后永远不再尝试。 const BAD_TTL = 5 * 60 * 1000; // 失败镜像的冷却时间 const stateCache = new Map(); // prefix -> { current, bad: Map } function stateOf(prefix) { let c = stateCache.get(prefix); if (!c) { c = { current: null, bad: new Map() }; stateCache.set(prefix, c); } return c; } function isBad(c, mirror) { const ts = c.bad.get(mirror); if (!ts) return false; if (Date.now() - ts > BAD_TTL) { c.bad.delete(mirror); return false; } return true; } function markBad(prefix, mirror) { const c = stateOf(prefix); c.bad.set(mirror, Date.now()); if (c.current === mirror) c.current = null; } function markGood(prefix, mirror) { const c = stateOf(prefix); c.current = mirror; c.bad.delete(mirror); } // 内容级失败(找不到资源、凭据错误、页面解析不出)说明镜像本身是通的, // 不能拉黑,否则一次密码输错或一次冷门查询就会废掉全部镜像。 function contentError(message) { const err = new Error(message); err.mirrorHealthy = true; return err; } function isMirrorFault(e) { return !(e && e.mirrorHealthy); } // 竞速败者是被我们自己中止的,不能据此判定镜像坏掉 function isCancelled(e) { return !!e && (e.name === 'AbortError' || /请求已取消/.test(e.message || '')); } // 候选顺序:上次成功的优先,其余按原顺序,已拉黑的排到最后兜底 function candidates(prefix, mirrors) { const c = stateOf(prefix); const fresh = []; const stale = []; for (const m of mirrors) { if (m === c.current) continue; (isBad(c, m) ? stale : fresh).push(m); } const out = []; if (c.current && mirrors.includes(c.current)) out.push(c.current); out.push(...fresh, ...stale); return out.length ? out : mirrors.slice(); } /** * 串行尝试:按优先级逐个调用 fn(mirror),第一个成功即返回。 * 适用于镜像少、需要严格优先级的场景。 */ async function tryMirrors(prefix, mirrors, fn) { const list = candidates(prefix, mirrors); let lastErr; for (const m of list) { try { const r = await fn(m); markGood(prefix, m); return r; } catch (e) { lastErr = e; // 镜像可达但内容不满足时,说明换镜像也是同样结果,直接返回 if (!isMirrorFault(e)) { markGood(prefix, m); throw e; } markBad(prefix, m); } } throw lastErr || new Error('所有镜像均不可用'); } /** * 竞速尝试:同时向所有候选镜像发起请求,最先成功的胜出。 * 适用于镜像多且大量失效的场景(如 LibGen),避免串行等待累加。 */ // 竞速时只用未拉黑的镜像;全被拉黑才退回完整列表重试一轮。 function raceCandidates(prefix, mirrors) { const c = stateOf(prefix); const fresh = candidates(prefix, mirrors).filter((m) => m === c.current || !isBad(c, m)); return fresh.length ? fresh : mirrors.slice(); } async function raceMirrors(prefix, mirrors, fn) { const list = raceCandidates(prefix, mirrors); if (!list.length) throw new Error('没有可用镜像'); // 胜出后主动中止其余在途请求,避免败者继续占用连接与代理带宽 const ac = new AbortController(); return new Promise((resolve, reject) => { let pending = list.length; let settled = false; let lastErr; const settle = (fn2, value) => { if (settled) return; settled = true; ac.abort(); fn2(value); }; for (const m of list) { Promise.resolve() .then(() => fn(m, ac.signal)) .then((r) => { if (settled) return; markGood(prefix, m); settle(resolve, r); }) .catch((e) => { // 输掉竞速被我们主动中止不算故障;但真实故障即使输了也要记进黑名单, // 否则下次仍会去竞速一个已知坏掉的镜像。 if (!isCancelled(e) && isMirrorFault(e)) markBad(prefix, m); if (settled) return; if (!isMirrorFault(e)) { markGood(prefix, m); settle(reject, e); return; } lastErr = e; if (--pending === 0) settle(reject, lastErr || new Error('所有镜像均不可用')); }); } }); } module.exports = { tryMirrors, raceMirrors, contentError };