feat(server,fs): W5-E production fsworker role, settings auto-investigate mode, replace old file to trash_item
- main.ts: `--role fsworker` dispatched before the rest of the module graph is loaded (other imports became dynamic); runs fsWorkerMain from the new `@umt/adapters/fsworker` export (~150 ms startup in the compiled binary) - fsproxy: default worker command picked per runtime (deno CLI -> `deno run fsworker.ts`, compiled binary -> `<execPath> --role fsworker`); stats expose workerKind; UMT_FS_TEST_HANG_AFTER_BYTES acceptance switch for containers - compile: --allow-run adds /app/umt-server (spawn self); start task allows deno - organize: replaced old file moved into the file-manager trash layout (<root>/.umt-trash/<day>/<uuid>/<rel>) and registered in trash_item, ledger result follows; organize.transfer log carries fsproxy stats - settings: autoInvestigateMode (investigate/recover) + max recover attempts - deploy.md: container verification (subprocess self, TIMEOUT -> SIGKILL x3) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fc49c58359
commit
3641ab9573
12 files changed
+248
-48
No files matched your search
@@ -7,7 +7,9 @@
|
||||
//
|
||||
// 与 MP 的差异:MP 是常驻 worker + 一把锁串行;我们每个操作一个子进程(~50 ms 启动,入库按文件计可忽略),
|
||||
// 天然并发、互不拖累,也没有"上一个请求挂死把 worker 占住"的问题。
|
||||
// 无法起子进程时(deno compile 出的二进制:execPath 不是 deno;或缺 --allow-run)回退 in-process:
|
||||
// 启动方式:execPath 是 deno CLI(开发/测试)→ `deno run -A fsworker.ts`;否则视为 deno compile 出的 umt-server
|
||||
// 二进制 → `<execPath> --role fsworker`(入口 main.ts 在加载其余模块前分派)。编译产物的 --allow-run 须含自身绝对路径
|
||||
// (镜像内 /app/umt-server,见 apps/server/deno.json);起不来(NotCapable 等)回退 in-process:
|
||||
// 同样的心跳看门狗,超时立即以 TIMEOUT 失败并中止操作,但卡死的系统调用占住的线程无法回收(见 README)。
|
||||
import { basename } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -51,6 +53,7 @@ export interface FsCallOptions {
|
||||
|
||||
export interface FsProxyStats {
|
||||
mode: "subprocess" | "inprocess";
|
||||
workerKind: FsProxy["workerKind"];
|
||||
spawned: number;
|
||||
timeouts: number;
|
||||
killed: number;
|
||||
@@ -75,13 +78,48 @@ const WORKER_PATH = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
function denoCliAvailable(): boolean {
|
||||
/** 当前运行时能用的缺省 worker 启动命令:deno CLI → run 脚本;编译二进制 → 自身 --role fsworker;都不行 → null */
|
||||
export function defaultWorkerCommand(): { cmd: string; args: string[]; kind: "script" | "self" } | null {
|
||||
const D = (globalThis as { Deno?: { execPath?: () => string; Command?: unknown } }).Deno;
|
||||
if (!D?.Command || !D.execPath || !WORKER_PATH) return false;
|
||||
if (!D?.Command || !D.execPath) return null;
|
||||
let exe: string;
|
||||
try {
|
||||
return /^deno(\.exe)?$/.test(basename(D.execPath()));
|
||||
exe = D.execPath();
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
if (/^deno(\.exe)?$/.test(basename(exe))) {
|
||||
if (!WORKER_PATH) return null;
|
||||
return {
|
||||
cmd: exe,
|
||||
args: [
|
||||
"run",
|
||||
"--no-config",
|
||||
"--quiet",
|
||||
"--no-prompt",
|
||||
// Deno 对 /dev、/proc 下的路径(如 /dev/shm 上的库根)要求 --allow-all;worker 只执行本包固定的操作表
|
||||
"--allow-all",
|
||||
WORKER_PATH,
|
||||
],
|
||||
kind: "script",
|
||||
};
|
||||
}
|
||||
return { cmd: exe, args: ["--role", "fsworker"], kind: "self" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅容器/手工验收用:UMT_FS_TEST_HANG_AFTER_BYTES=<n> 让每个数据类操作的 worker 在读到 n 字节后卡死 JS 线程
|
||||
* (不响应 SIGTERM),用于在生产二进制里验证 停滞超时 → SIGKILL → TIMEOUT。只在子进程模式生效。
|
||||
*/
|
||||
function envTestHook(): FsTestHook | undefined {
|
||||
try {
|
||||
const v = (globalThis as { Deno?: { env?: { get(k: string): string | undefined } } }).Deno?.env?.get(
|
||||
"UMT_FS_TEST_HANG_AFTER_BYTES",
|
||||
);
|
||||
if (v === undefined || v === "" || !/^\d+$/.test(v)) return undefined;
|
||||
return { hangAfterBytes: Number(v), ignoreTerm: true };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +147,8 @@ function pathOf(req: FsOpRequest): string | undefined {
|
||||
|
||||
export class FsProxy {
|
||||
readonly cfg: Required<Omit<FsProxyConfig, "workerCommand" | "mode">> & Pick<FsProxyConfig, "workerCommand">;
|
||||
/** 子进程怎么起:script = deno run fsworker.ts;self = 编译二进制 --role fsworker;custom = 显式 workerCommand */
|
||||
readonly workerKind: "script" | "self" | "custom" | null;
|
||||
#mode: "subprocess" | "inprocess";
|
||||
#forced: boolean;
|
||||
#stats = { spawned: 0, timeouts: 0, killed: 0 };
|
||||
@@ -120,12 +160,15 @@ export class FsProxy {
|
||||
quickTimeoutMs: cfg.quickTimeoutMs ?? 30_000,
|
||||
progressIntervalMs: cfg.progressIntervalMs ?? 1000,
|
||||
killGraceMs: cfg.killGraceMs ?? 2000,
|
||||
...(cfg.workerCommand ? { workerCommand: cfg.workerCommand } : {}),
|
||||
};
|
||||
const auto = cfg.workerCommand ? null : defaultWorkerCommand();
|
||||
const wc = cfg.workerCommand ?? (auto ? { cmd: auto.cmd, args: auto.args } : undefined);
|
||||
if (wc) this.cfg.workerCommand = wc;
|
||||
this.workerKind = cfg.workerCommand ? "custom" : (auto?.kind ?? null);
|
||||
const mode = cfg.mode ?? "auto";
|
||||
this.#forced = mode === "subprocess";
|
||||
if (mode === "inprocess") this.#mode = "inprocess";
|
||||
else if (cfg.workerCommand || denoCliAvailable()) this.#mode = "subprocess";
|
||||
else if (wc) this.#mode = "subprocess";
|
||||
else {
|
||||
if (mode === "subprocess") throw new Error("fsproxy: 当前运行时无法启动 deno 子进程,且未提供 workerCommand");
|
||||
this.#mode = "inprocess";
|
||||
@@ -138,7 +181,7 @@ export class FsProxy {
|
||||
}
|
||||
|
||||
stats(): FsProxyStats {
|
||||
return { mode: this.#mode, ...this.#stats, fallbackReason: this.#fallbackReason };
|
||||
return { mode: this.#mode, workerKind: this.workerKind, ...this.#stats, fallbackReason: this.#fallbackReason };
|
||||
}
|
||||
|
||||
// ── 业务操作 ──
|
||||
@@ -260,18 +303,7 @@ export class FsProxy {
|
||||
|
||||
async #runSubprocess(req: FsOpRequest, call: FsCallOptions): Promise<unknown> {
|
||||
const D = (globalThis as unknown as { Deno: DenoLike }).Deno;
|
||||
const cmd = this.cfg.workerCommand ?? {
|
||||
cmd: D.execPath(),
|
||||
args: [
|
||||
"run",
|
||||
"--no-config",
|
||||
"--quiet",
|
||||
"--no-prompt",
|
||||
// Deno 对 /dev、/proc 下的路径(如 /dev/shm 上的库根)要求 --allow-all;worker 只执行本包固定的操作表
|
||||
"--allow-all",
|
||||
WORKER_PATH!,
|
||||
],
|
||||
};
|
||||
const cmd = this.cfg.workerCommand!;
|
||||
let child: DenoChild;
|
||||
try {
|
||||
child = new D.Command(cmd.cmd, {
|
||||
@@ -287,7 +319,8 @@ export class FsProxy {
|
||||
this.#stats.spawned += 1;
|
||||
const limit = this.#limitFor(req);
|
||||
const w = child.stdin.getWriter();
|
||||
await w.write(new TextEncoder().encode(JSON.stringify(call._test ? { ...req, _test: call._test } : req)));
|
||||
const hook = call._test ?? (DATA_OPS.has(req.op) ? envTestHook() : undefined);
|
||||
await w.write(new TextEncoder().encode(JSON.stringify(hook ? { ...req, _test: hook } : req)));
|
||||
await w.close().catch(() => undefined);
|
||||
|
||||
let stderr = "";
|
||||
|
||||
Reference in new issue
Block a user