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:
gsxdsm
2026-06-12 17:21:14 -07:00
parent 0cd2ae1032
commit 21cb8abcf3
2 changed files with 85 additions and 2 deletions

View File

@@ -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}`);

View File

@@ -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);
});
});