From 21cb8abcf3634398e25ac2cffa30ee812a620255 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 12 Jun 2026 17:21:14 -0700 Subject: [PATCH] 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 --- .../core/src/__test-utils__/vitest-setup.ts | 21 +++++- .../vitest-setup-tmp-redirect.test.ts | 66 ++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__test-utils__/vitest-setup.ts b/packages/core/src/__test-utils__/vitest-setup.ts index c9f461a1fe..6aa4bb6c29 100644 --- a/packages/core/src/__test-utils__/vitest-setup.ts +++ b/packages/core/src/__test-utils__/vitest-setup.ts @@ -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}`); diff --git a/packages/core/src/__tests__/vitest-setup-tmp-redirect.test.ts b/packages/core/src/__tests__/vitest-setup-tmp-redirect.test.ts index 0d5885d15f..4eaa544fbf 100644 --- a/packages/core/src/__tests__/vitest-setup-tmp-redirect.test.ts +++ b/packages/core/src/__tests__/vitest-setup-tmp-redirect.test.ts @@ -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); + }); });