The throttle could drop the final copy heartbeat when it landed inside the interval (fsproxy.test saw 6291456 instead of 6291461 under ci load). Phase completion (bytes >= total) is now sent immediately like phase changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
// fsproxy 执行子进程:从 stdin 读一条 JSON 请求,执行单个文件操作,stdout 逐行 JSON 上报
|
|
// {t:"hb",phase,bytes,total}(按 progressIntervalMs 节流,阶段切换与阶段完成立即报)/ {t:"ok",result} / {t:"err",code,message}。
|
|
// SIGTERM → 中止当前操作(copy 会清掉自己的 partial)后退出;挂死在系统调用里时由父进程 SIGKILL 回收。
|
|
// 只依赖 node 内置模块,`deno run --no-config` 毫秒级启动。编译二进制可在入口按 --role fsworker 调 fsWorkerMain()。
|
|
import { toFsError } from "./errors.ts";
|
|
import { type FsOpRequest, type FsProgress, runFsOp } from "./fsops.ts";
|
|
|
|
const enc = new TextEncoder();
|
|
|
|
function send(obj: unknown): void {
|
|
try {
|
|
Deno.stdout.writeSync(enc.encode(`${JSON.stringify(obj)}\n`));
|
|
} catch {
|
|
// 父进程已退出:无处可报
|
|
}
|
|
}
|
|
|
|
/** 阻塞 JS 线程(测试用,模拟卡死的系统调用:信号监听器也不会运行) */
|
|
function blockForever(): never {
|
|
const a = new Int32Array(new SharedArrayBuffer(4));
|
|
for (;;) Atomics.wait(a, 0, 0);
|
|
}
|
|
|
|
export async function fsWorkerMain(progressIntervalMs = Number(Deno.env.get("UMT_FS_PROGRESS_INTERVAL_MS") ?? 1000)) {
|
|
const req = JSON.parse(await new Response(Deno.stdin.readable).text()) as FsOpRequest;
|
|
const ac = new AbortController();
|
|
Deno.addSignalListener("SIGTERM", () => {
|
|
if (req._test?.ignoreTerm) return;
|
|
ac.abort();
|
|
// 操作卡在系统调用里时 abort 解不开:给清理一点时间后直接退出
|
|
setTimeout(() => Deno.exit(143), 500);
|
|
});
|
|
let lastAt = 0;
|
|
let lastPhase = "";
|
|
const progress = (p: FsProgress) => {
|
|
const hang = req._test?.hangAfterBytes;
|
|
const now = Date.now();
|
|
// 阶段切换、阶段完成(bytes 到 total)立即报,其余按间隔节流——否则最后一跳可能被节流吞掉
|
|
const phaseDone = p.total > 0 && p.bytes >= p.total;
|
|
if (
|
|
p.phase !== lastPhase ||
|
|
phaseDone ||
|
|
now - lastAt >= progressIntervalMs ||
|
|
(hang !== undefined && p.bytes >= hang)
|
|
) {
|
|
lastAt = now;
|
|
lastPhase = p.phase;
|
|
send({ t: "hb", ...p });
|
|
}
|
|
if (hang !== undefined && p.bytes >= hang) blockForever();
|
|
};
|
|
if (req._test?.hangAfterBytes === 0) {
|
|
send({ t: "hb", phase: "start", bytes: 0, total: 0 });
|
|
blockForever();
|
|
}
|
|
try {
|
|
const result = await runFsOp(req, { signal: ac.signal, progress });
|
|
send({ t: "ok", result });
|
|
} catch (e) {
|
|
const fe = toFsError(e);
|
|
send({
|
|
t: "err",
|
|
code: ac.signal.aborted ? "ABORTED" : fe.code,
|
|
message: fe.message,
|
|
...(fe.detail.path ? { path: fe.detail.path } : {}),
|
|
...(fe.detail.syscall ? { syscall: fe.detail.syscall } : {}),
|
|
});
|
|
}
|
|
Deno.exit(0);
|
|
}
|
|
|
|
if (import.meta.main) await fsWorkerMain();
|