FN-6310: recreate temp redirect sink after cleanup
Recreate Vitest tmpdir redirect sinks on demand after recovery cleanup removes them. - Ensure cached tmpdir redirect sinks are recreated before later mkdtemp calls. - Preserve live/current redirect sinks when sweeping stale pid-owned sinks. - Add regression coverage for sync/async mkdtemp recovery and stale sink sweeping. Files changed: packages/core/src/__test-utils__/vitest-setup.ts | 21 ++++++- .../__tests__/vitest-setup-tmp-redirect.test.ts | 66 +++++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-6310 Fusion-Task-Lineage: 311cb7f6-9c7b-4e4d-9235-e787382dcc3a
This commit is contained in:
@@ -204,6 +204,8 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
|
||||
function removeTmpdirRedirectSinkForPid(ownerPid: number): void {
|
||||
if (ownerPid === process.pid || isProcessAlive(ownerPid)) return;
|
||||
|
||||
try {
|
||||
rmSync(join(WORKER_ROOT, `redir-${ownerPid}`), { recursive: true, force: true });
|
||||
} catch {
|
||||
@@ -264,8 +266,25 @@ function sweepDeadTmpdirRedirectSinks(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export const __fusionTmpdirRedirectTestHooks = {
|
||||
workerRoot: WORKER_ROOT,
|
||||
registryPath: TMPDIR_REDIRECT_REGISTRY,
|
||||
sinkForPid(pid: number): string {
|
||||
return join(WORKER_ROOT, `redir-${pid}`);
|
||||
},
|
||||
resetSweepForTest(): void {
|
||||
tmpdirRedirectSweepComplete = false;
|
||||
},
|
||||
sweepDeadTmpdirRedirectSinks,
|
||||
};
|
||||
|
||||
function ensureTmpdirRedirectSink(): string {
|
||||
if (tmpdirRedirectSink) return tmpdirRedirectSink;
|
||||
if (tmpdirRedirectSink) {
|
||||
// FN-6310: recovery-timeout cleanup can remove a live worker's cached
|
||||
// redirect sink; recreate it on demand so later mkdtemp calls don't ENOENT.
|
||||
mkdirSync(tmpdirRedirectSink, { recursive: true });
|
||||
return tmpdirRedirectSink;
|
||||
}
|
||||
|
||||
sweepDeadTmpdirRedirectSinks();
|
||||
const sink = join(WORKER_ROOT, `redir-${process.pid}`);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, mkdirSync, rmSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, sep } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { __fusionTmpdirRedirectTestHooks } from "../__test-utils__/vitest-setup";
|
||||
|
||||
const createdPaths: string[] = [];
|
||||
|
||||
@@ -18,6 +19,11 @@ function expectUnderWorkerRoot(path: string): void {
|
||||
expect(dirname(path)).toBe(join(workerRoot!, `redir-${process.pid}`));
|
||||
}
|
||||
|
||||
function rememberDir(path: string): string {
|
||||
mkdirSync(path, { recursive: true });
|
||||
return remember(path);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const path of createdPaths.splice(0).reverse()) {
|
||||
rmSync(path, { recursive: true, force: true });
|
||||
@@ -65,4 +71,62 @@ describe("vitest setup tmpdir mkdtemp redirect", () => {
|
||||
|
||||
expect(path.startsWith(`${tmpdir()}${sep}`)).toBe(true);
|
||||
});
|
||||
|
||||
it("recreates the cached redirect sink after deletion for sync and async mkdtemp", async () => {
|
||||
const sink = __fusionTmpdirRedirectTestHooks.sinkForPid(process.pid);
|
||||
const first = remember(mkdtempSync(join(tmpdir(), "fn-redirect-prime-")));
|
||||
expectUnderWorkerRoot(first);
|
||||
expect(existsSync(sink)).toBe(true);
|
||||
|
||||
rmSync(sink, { recursive: true, force: true });
|
||||
expect(existsSync(sink)).toBe(false);
|
||||
|
||||
const syncPath = remember(mkdtempSync(join(tmpdir(), "fn-redirect-recreated-sync-")));
|
||||
expect(existsSync(syncPath)).toBe(true);
|
||||
expect(dirname(syncPath)).toBe(sink);
|
||||
expect(existsSync(sink)).toBe(true);
|
||||
|
||||
rmSync(sink, { recursive: true, force: true });
|
||||
expect(existsSync(sink)).toBe(false);
|
||||
|
||||
const asyncPath = remember(await mkdtemp(join(tmpdir(), "fn-redirect-recreated-async-")));
|
||||
expect(existsSync(asyncPath)).toBe(true);
|
||||
expect(dirname(asyncPath)).toBe(sink);
|
||||
expect(existsSync(sink)).toBe(true);
|
||||
});
|
||||
|
||||
it("sweeps only dead redirect sinks and preserves current or alive pids", () => {
|
||||
const { registryPath, resetSweepForTest, sinkForPid, sweepDeadTmpdirRedirectSinks } = __fusionTmpdirRedirectTestHooks;
|
||||
const currentSink = rememberDir(sinkForPid(process.pid));
|
||||
const liveForeignPid = process.ppid;
|
||||
const liveForeignSink = rememberDir(sinkForPid(liveForeignPid));
|
||||
const deadPid = 99_999_999;
|
||||
const deadSink = rememberDir(sinkForPid(deadPid));
|
||||
|
||||
writeFileSync(registryPath, `${process.pid}\n${process.pid}\n${liveForeignPid}\n${deadPid}\n`);
|
||||
resetSweepForTest();
|
||||
sweepDeadTmpdirRedirectSinks();
|
||||
|
||||
expect(existsSync(currentSink)).toBe(true);
|
||||
expect(existsSync(liveForeignSink)).toBe(true);
|
||||
expect(existsSync(deadSink)).toBe(false);
|
||||
|
||||
const fallbackDeadSink = rememberDir(sinkForPid(deadPid - 1));
|
||||
rmSync(registryPath, { force: true });
|
||||
resetSweepForTest();
|
||||
sweepDeadTmpdirRedirectSinks();
|
||||
|
||||
expect(existsSync(currentSink)).toBe(true);
|
||||
expect(existsSync(liveForeignSink)).toBe(true);
|
||||
expect(existsSync(fallbackDeadSink)).toBe(false);
|
||||
|
||||
const emptyRegistryDeadSink = rememberDir(sinkForPid(deadPid - 2));
|
||||
writeFileSync(registryPath, "");
|
||||
resetSweepForTest();
|
||||
sweepDeadTmpdirRedirectSinks();
|
||||
|
||||
expect(existsSync(currentSink)).toBe(true);
|
||||
expect(existsSync(liveForeignSink)).toBe(true);
|
||||
expect(existsSync(emptyRegistryDeadSink)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user