diff --git a/src/restore-engine.ts b/src/restore-engine.ts index 1f11431..c0e142b 100644 --- a/src/restore-engine.ts +++ b/src/restore-engine.ts @@ -1472,6 +1472,9 @@ async function countRowsInDumpOrThrow(dumpPath: string, signal?: AbortSignal): P try { const listFile = join(listDir, "table-data.list"); await writeFile(listFile, dataEntries.join("\n") + "\n"); + // 建临时目录、写清单的这两步里也可能被叫停。abort 事件已经发过,后面挂的监听收不到, + // 不在这里再看一眼,起来的 pg_restore 就会把整份 dump 读完。这一眼和下面挂监听在同一拍里,中间没有空档 + if (signal?.aborted) return null; return await new Promise((resolve) => { // 和 pgRestoreOutput 一样钉 C locale const proc = spawn(bin, ["--data-only", `--use-list=${listFile}`, "--file=-", dumpPath], { diff --git a/test/drill-row-counts.test.js b/test/drill-row-counts.test.js index 5bff566..3295340 100644 --- a/test/drill-row-counts.test.js +++ b/test/drill-row-counts.test.js @@ -5,7 +5,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { createHash } from "node:crypto"; -import { readFileSync, mkdtempSync, rmSync } from "node:fs"; +import { readFileSync, mkdtempSync, rmSync, mkdirSync, writeFileSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { drillDump, verifyRestored } from "../dist/drill.js"; @@ -118,6 +118,35 @@ test( const stopped = countRowsInDump(dumpPath, stop.signal); stop.abort(); assert.equal(await stopped, null); + // 在建临时目录到起 pg_restore 之间叫停:abort 事件发在挂监听之前,也不能再起 pg_restore。 + // 时机靠真 AbortSignal 卡不准,用一个"临时目录一建出来就算已叫停"的 signal 把它钉死; + // 包一层 pg_restore 记下每次调用,看有没有起 --data-only 那一遍 + const scratch = join(tmp, "scratch"); + mkdirSync(scratch); + const calls = join(tmp, "pg_restore.calls"); + const wrapper = join(tmp, "pg_restore-wrapper.sh"); + writeFileSync(wrapper, `#!/bin/sh\necho "$@" >> '${calls}'\nexec '${pgRestore}' "$@"\n`, { mode: 0o755 }); + const lateStop = { + get aborted() { + return readdirSync(scratch).some((name) => name.startsWith("backupdrill-rowcount-")); + }, + addEventListener() {}, + removeEventListener() {}, + }; + const savedEnv = { TMPDIR: process.env.TMPDIR, BACKUPDRILL_PG_RESTORE: process.env.BACKUPDRILL_PG_RESTORE }; + process.env.TMPDIR = scratch; + process.env.BACKUPDRILL_PG_RESTORE = wrapper; + try { + assert.equal(await countRowsInDump(dumpPath, lateStop), null); + } finally { + for (const [name, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } + const invoked = readFileSync(calls, "utf8"); + assert.match(invoked, /--list/, "the wrapper was used"); + assert.doesNotMatch(invoked, /--data-only/, "no pg_restore started after the caller stopped"); // 临时目录建不了:返回 null(演练判"数不出来"),不能抛出去变成没人接的 rejection const savedTmp = process.env.TMPDIR; process.env.TMPDIR = join(tmp, "no-such-dir");