authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-05 21:38:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 21:38:20-07:00
logbfe40e6d2574447b0651bdb41337db743ddbe12c
tree4d8460b216d9076f68bbd17a53be23b8106980b7
parent327a29ef56e215c69a5db5d0b6f8df89f04f5d5b
signature Signed by PGP key B5690EEEBB952194

fix(server): the PR cache sweep only removes real entry files (#16285)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

2 files changed, 8 insertions(+), 3 deletions(-)

apps/server/src/pullRequest/PullRequestReadCache.test.ts+5-2
......@@ -44,18 +44,21 @@ it.layer(NodeServices.layer)("PR filesystem cache", (it) => {
4444 );
4545 const unrelated = "unrelated.json";
4646 yield* fs.writeFileString(`${directory}/${unrelated}`, "{}");
47 // Starts with the store id but is not an entry file.
48 const prefixed = "pr-v2-backup.json";
49 yield* fs.writeFileString(`${directory}/${prefixed}`, "{}");
4750 const justExpired = Duration.sum(
4851 PullRequestReadCache.ENTRY_FILE_MAX_AGE,
4952 Duration.seconds(1),
5053 );
51 yield* ageFiles(directory, [stale!, revisions!, unrelated], justExpired);
54 yield* ageFiles(directory, [stale!, revisions!, unrelated, prefixed], justExpired);
5255 yield* ageFiles(directory, [fresh!], PullRequestReadCache.ENTRY_FILE_MAX_AGE);
5356
5457 yield* PullRequestReadCache.pruneExpiredEntryFiles(directory);
5558
5659 assert.deepStrictEqual(
5760 (yield* fs.readDirectory(directory)).toSorted(),
58 [fresh!, revisions!, unrelated].toSorted(),
61 [fresh!, revisions!, unrelated, prefixed].toSorted(),
5962 );
6063 }),
6164 );
apps/server/src/pullRequest/PullRequestReadCache.ts+3-1
......@@ -27,6 +27,8 @@ const CONCURRENT_READS = 512;
2727// Persistence prefixes every entry key with the store id, so each entry file
2828// name starts with it. The scope `revisions` file does not.
2929const STORE_ID = "pr-v2";
30// An entry file is the store id followed by the SHA-256 hex digest of its key.
31const ENTRY_FILE_NAME = new RegExp(`^${STORE_ID}[0-9a-f]{64}$`);
3032/**
3133 * Entries expire a minute after they are written, and a write sets the file's
3234 * mtime. A file untouched for a day is long expired; the day only leaves slack
......@@ -186,7 +188,7 @@ export const pruneExpiredEntryFiles = Effect.fn("PullRequestReadCache.pruneExpir
186188 const path = yield* Path.Path;
187189 const cutoff = (yield* Clock.currentTimeMillis) - Duration.toMillis(ENTRY_FILE_MAX_AGE);
188190 const entries = (yield* fileSystem.readDirectory(directory)).filter((name) =>
189 name.startsWith(STORE_ID),
191 ENTRY_FILE_NAME.test(name),
190192 );
191193 // One file at a time, and `partition` visits every file, so one locked file
192194 // does not stop the sweep.