Skip to content

Commit d4c7ae5

Browse files
shiminshenpenalosa
authored andcommitted
[wrangler] sweep stale .wrangler/tmp/* dirs at startup (#13930)
1 parent 97d4681 commit d4c7ae5

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Sweep stale `.wrangler/tmp/*` dirs left behind by abnormal exits
6+
7+
A `wrangler dev` session creates `.wrangler/tmp/bundle-*` and `.wrangler/tmp/dev-*` directories at startup and removes them via a `signal-exit` hook on graceful shutdown. When the process exited abnormally (SIGKILL, OOM, host crash) those directories were left behind and accumulated across sessions, slowing down dependency-walking tools that follow the bundle-emitted absolute-path imports.
8+
9+
`wrangler` now sweeps entries in `.wrangler/tmp/` older than 24 hours when a new temporary directory is requested, bounding the leak regardless of how prior sessions exited.

packages/wrangler/src/__tests__/paths.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import fs from "node:fs";
12
import * as path from "node:path";
23
import { describe, it } from "vitest";
34
import {
45
getBasePath,
56
getWranglerHiddenDirPath,
67
readableRelative,
8+
sweepStaleWranglerTmpDirs,
79
} from "../paths";
10+
import { runInTempDir } from "./helpers/run-in-tmp";
811

912
describe("paths", () => {
1013
describe("getBasePath()", () => {
@@ -39,6 +42,54 @@ describe("paths", () => {
3942
});
4043
});
4144

45+
describe("sweepStaleWranglerTmpDirs()", () => {
46+
runInTempDir();
47+
48+
const ageDir = (dir: string, ageMs: number): void => {
49+
const seconds = (Date.now() - ageMs) / 1000;
50+
fs.utimesSync(dir, seconds, seconds);
51+
};
52+
53+
it("removes orphaned dirs older than the staleness threshold", ({
54+
expect,
55+
}) => {
56+
const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp");
57+
fs.mkdirSync(tmpRoot, { recursive: true });
58+
const stale = path.join(tmpRoot, "bundle-stale");
59+
const fresh = path.join(tmpRoot, "bundle-fresh");
60+
fs.mkdirSync(stale);
61+
fs.mkdirSync(fresh);
62+
ageDir(stale, 2 * 24 * 60 * 60 * 1000);
63+
64+
sweepStaleWranglerTmpDirs(tmpRoot);
65+
66+
expect(fs.existsSync(stale)).toBe(false);
67+
expect(fs.existsSync(fresh)).toBe(true);
68+
});
69+
70+
it("does not throw when the tmp root is missing", ({ expect }) => {
71+
const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp");
72+
expect(() => sweepStaleWranglerTmpDirs(tmpRoot)).not.toThrow();
73+
});
74+
75+
it("only sweeps a given root once per process", ({ expect }) => {
76+
const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp");
77+
fs.mkdirSync(tmpRoot, { recursive: true });
78+
const orphan = path.join(tmpRoot, "bundle-orphan");
79+
fs.mkdirSync(orphan);
80+
ageDir(orphan, 2 * 24 * 60 * 60 * 1000);
81+
82+
sweepStaleWranglerTmpDirs(tmpRoot);
83+
expect(fs.existsSync(orphan)).toBe(false);
84+
85+
// Recreate an equally stale entry; the cached root must skip rescanning.
86+
fs.mkdirSync(orphan);
87+
ageDir(orphan, 2 * 24 * 60 * 60 * 1000);
88+
sweepStaleWranglerTmpDirs(tmpRoot);
89+
expect(fs.existsSync(orphan)).toBe(true);
90+
});
91+
});
92+
4293
describe("readableRelative", () => {
4394
const base = process.cwd();
4495

packages/wrangler/src/paths.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,55 @@ export function getWranglerHiddenDirPath(
9292
return path.join(projectRoot, ".wrangler");
9393
}
9494

95+
/**
96+
* Maximum age of a `.wrangler/tmp/*` entry before we treat it as orphaned and
97+
* eligible for the startup sweep. Tuned to avoid touching any directory a
98+
* concurrent wrangler session might still own.
99+
*/
100+
const STALE_WRANGLER_TMP_DIR_MS = 24 * 60 * 60 * 1000;
101+
102+
/**
103+
* Tracks tmp roots already swept by this process so repeated
104+
* `getWranglerTmpDir` calls within one wrangler invocation only scan once.
105+
*/
106+
const sweptTmpRoots = new Set<string>();
107+
108+
/**
109+
* Removes stale `.wrangler/tmp/*` entries left behind by previous wrangler
110+
* sessions that exited abnormally (SIGKILL, OOM, host crash) and so missed
111+
* the `signal-exit` cleanup. Runs at most once per tmp root per process.
112+
*
113+
* Exported for tests.
114+
*/
115+
export function sweepStaleWranglerTmpDirs(tmpRoot: string): void {
116+
if (sweptTmpRoots.has(tmpRoot)) {
117+
return;
118+
}
119+
sweptTmpRoots.add(tmpRoot);
120+
121+
let entries: fs.Dirent[];
122+
try {
123+
entries = fs.readdirSync(tmpRoot, { withFileTypes: true });
124+
} catch {
125+
return;
126+
}
127+
128+
const cutoff = Date.now() - STALE_WRANGLER_TMP_DIR_MS;
129+
for (const entry of entries) {
130+
if (!entry.isDirectory()) {
131+
continue;
132+
}
133+
const entryPath = path.join(tmpRoot, entry.name);
134+
try {
135+
if (fs.statSync(entryPath).mtimeMs < cutoff) {
136+
removeDirSync(entryPath);
137+
}
138+
} catch {
139+
/* best effort — another process may have removed it first */
140+
}
141+
}
142+
}
143+
95144
/**
96145
* Gets a temporary directory in the project's `.wrangler` folder with the
97146
* specified prefix. We create temporary directories in `.wrangler` as opposed
@@ -106,6 +155,7 @@ export function getWranglerTmpDir(
106155
): EphemeralDirectory {
107156
const tmpRoot = path.join(getWranglerHiddenDirPath(projectRoot), "tmp");
108157
fs.mkdirSync(tmpRoot, { recursive: true });
158+
sweepStaleWranglerTmpDirs(tmpRoot);
109159

110160
const tmpPrefix = path.join(tmpRoot, `${prefix}-`);
111161
const tmpDir = fs.realpathSync(fs.mkdtempSync(tmpPrefix));

0 commit comments

Comments
 (0)