1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/** # 脚本说明
|
||||
* 压测清场:把某一轮发射器开过的浏览器全部停止。
|
||||
* 1) 读 data/<label>-launched.json 的每条记录(--scan 模式则改从节点列表取全部 IN_USE Profile)
|
||||
* 2) 逐个 GET /profiles/:fp 拿当前 leaseId → POST /profiles/:fp/browser/stop(x-lease-id;
|
||||
* 显式停止走 closing 路径,不会触发「UI 误关自动复活」)
|
||||
* 3) 兜底:对仍存的 user-data-dir 进程 SIGKILL(等待复活限频窗口过期,前后共两轮)
|
||||
* 用法:bun stop-browsers.ts --label test1 [--scan]
|
||||
*/
|
||||
const fs = require("fs") as typeof import("fs");
|
||||
const args = process.argv.slice(2);
|
||||
function argOf(k: string, d: string): string { const i = args.indexOf(k); return i >= 0 && i + 1 < args.length ? args[i + 1] : d; }
|
||||
const LABEL = argOf("--label", "test1");
|
||||
const SCAN = args.includes("--scan");
|
||||
const NODE = "http://127.0.0.1:3106";
|
||||
const DATA = `${new URL(".", import.meta.url).pathname}data`;
|
||||
|
||||
/** # 函数:docker exec 查 admin DB 单值(取 Token 用) **/
|
||||
function sqlOne(sql: string): string {
|
||||
const p = Bun.spawnSync(["docker", "exec", "serpmax-pg", "psql", "-U", "postgres", "-d", "serpmax_admin", "-tAc", sql], { stdout: "pipe", stderr: "pipe" });
|
||||
const out = p.stdout.toString().trim();
|
||||
if (p.exitCode !== 0 || !out) throw new Error(`SQL 失败`);
|
||||
return out;
|
||||
}
|
||||
const TOKEN = sqlOne("SELECT api_token FROM users WHERE id=2");
|
||||
const NODE_KEY = /^NODE_KEY=(.*)$/m.exec(fs.readFileSync("/opt/serpmax/node/.env", "utf8"))?.[1]?.trim() ?? "";
|
||||
const LEASE_TTL_MS = 120000;
|
||||
|
||||
const headersBase: Record<string, string> = { "content-type": "application/json", authorization: `Bearer ${TOKEN}`, "x-node-key": NODE_KEY };
|
||||
|
||||
/** # 成员变量 待停清单:label 文件或节点扫描(IN_USE/有活动 lease 的 Profile) **/
|
||||
let targets: { fp: string }[] = [];
|
||||
if (SCAN) {
|
||||
const res = await fetch(`${NODE}/profiles`, { headers: headersBase, signal: AbortSignal.timeout(60000) });
|
||||
const j: any = await res.json();
|
||||
const rows: any[] = j?.data?.profiles ?? j?.profiles ?? [];
|
||||
targets = rows.filter((r) => {
|
||||
const lease = r?.lease ?? null;
|
||||
return (lease && new Date(lease.expiresAt).getTime() > Date.now()) || ["IN_USE"].includes(r?.state);
|
||||
}).map((r) => ({ fp: String(r.id) }));
|
||||
console.log(`[stop] 扫描到 ${targets.length} 个在用 Profile`);
|
||||
} else {
|
||||
let list: { fp: string }[] = [];
|
||||
try { list = JSON.parse(await Bun.file(`${DATA}/${LABEL}-launched.json`).text() || "[]"); } catch { list = []; }
|
||||
targets = list.map((e: any) => ({ fp: String(e.fp) }));
|
||||
}
|
||||
console.log(`[stop] 目标 ${targets.length} 个`);
|
||||
|
||||
/** # 函数:显式停止一个 Profile 的浏览器(先拿当前 leaseId,过期则补一个短 lease) **/
|
||||
async function stopOne(fp: string): Promise<void> {
|
||||
const headers = { ...headersBase } as Record<string, string>;
|
||||
try {
|
||||
const d = await fetch(`${NODE}/profiles/${fp}`, { headers: headersBase, signal: AbortSignal.timeout(10000) });
|
||||
const j: any = await d.json();
|
||||
const lease = j?.data?.lease ?? j?.lease;
|
||||
if (lease?.leaseId && new Date(lease.expiresAt).getTime() > Date.now()) headers["x-lease-id"] = lease.leaseId;
|
||||
} catch { /* detail 拿不到就直接停 */ }
|
||||
if (!headers["x-lease-id"]) {
|
||||
const r = await fetch(`${NODE}/profiles/${fp}/lease`, { method: "POST", headers: headersBase, body: JSON.stringify({ consumerId: "stress-stop", ttlMs: LEASE_TTL_MS }), signal: AbortSignal.timeout(10000) });
|
||||
if (r.ok) { const j2: any = await r.json(); headers["x-lease-id"] = (j2?.data ?? j2)?.leaseId; }
|
||||
}
|
||||
await fetch(`${NODE}/profiles/${fp}/browser/stop`, { method: "POST", headers, body: "{}", signal: AbortSignal.timeout(30000) }).catch(() => null);
|
||||
}
|
||||
|
||||
const workers = 10;
|
||||
let cursor = 0;
|
||||
let stopped = 0;
|
||||
async function worker(): Promise<void> {
|
||||
while (cursor < targets.length) {
|
||||
const e = targets[cursor++];
|
||||
await stopOne(e.fp);
|
||||
stopped++;
|
||||
if (stopped % 25 === 0) console.log(`[stop] ${stopped}/${targets.length}`);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: workers }, () => worker()));
|
||||
console.log(`[stop] API 显式停完 ${stopped}`);
|
||||
|
||||
/** # 函数:兜底统计存活压测浏览器进程数 **/
|
||||
function countLeft(): number {
|
||||
let n = 0;
|
||||
for (const pid of fs.readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
|
||||
try { if (fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").includes("--user-data-dir=")) n++; } catch { /* 已退出 */ }
|
||||
}
|
||||
return n;
|
||||
}
|
||||
for (const round of [1, 2]) {
|
||||
const left = countLeft();
|
||||
console.log(`[stop] round${round} 残存进程 ${left}`);
|
||||
if (left === 0) break;
|
||||
for (const pid of fs.readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
|
||||
try {
|
||||
if (fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").includes("--user-data-dir=")) process.kill(Number(pid), "SIGKILL");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 8000));
|
||||
}
|
||||
console.log(`[stop] done`);
|
||||
|
||||
Reference in New Issue
Block a user