diff --git a/server/services/arduino-compiler.ts b/server/services/arduino-compiler.ts index c6e8ac7e8..b1fe3edfe 100644 --- a/server/services/arduino-compiler.ts +++ b/server/services/arduino-compiler.ts @@ -362,6 +362,7 @@ export class ArduinoCompiler { await writeOutputToCache(this.defaultBinaryStorageDir, sketchHash, cliOutput).catch(() => undefined); } await runHexCacheCleanup(hexCacheDir); + await runHexCacheCleanup(this.defaultBinaryStorageDir); } return { cliOutput, cliErrors, parsedErrors }; diff --git a/server/services/compiler/cache-manager.ts b/server/services/compiler/cache-manager.ts index 3a7cfbfa6..08db80389 100644 --- a/server/services/compiler/cache-manager.ts +++ b/server/services/compiler/cache-manager.ts @@ -124,7 +124,7 @@ export async function runHexCacheCleanup( ): Promise { try { const entries = await readdir(hexCacheDir); - const files: Array<{ path: string; size: number; atimeMs: number }> = []; + const files: Array<{ path: string; sidecarPath: string; size: number; atimeMs: number }> = []; let totalSize = 0; for (const entry of entries) { @@ -136,6 +136,7 @@ export async function runHexCacheCleanup( totalSize += fileStat.size; files.push({ path: fullPath, + sidecarPath: join(hexCacheDir, `${entry.slice(0, -".hex".length)}.output.txt`), size: fileStat.size, atimeMs: fileStat.atimeMs || fileStat.mtimeMs, }); @@ -150,6 +151,8 @@ export async function runHexCacheCleanup( for (const file of files) { if (totalSize <= maxBytes) break; await rm(file.path, { force: true }); + // The output sidecar is useless without its binary and must not outlive it. + await rm(file.sidecarPath, { force: true }); totalSize -= file.size; } } catch (error) { diff --git a/server/services/workers/compile-worker-utils.ts b/server/services/workers/compile-worker-utils.ts index dedce9c88..9d4af219b 100644 --- a/server/services/workers/compile-worker-utils.ts +++ b/server/services/workers/compile-worker-utils.ts @@ -153,6 +153,18 @@ export async function evictLruEntries( } } +/** + * Directories under the build cache that grow per sketch hash and need LRU eviction. + * The instant-binary store (written by ArduinoCompiler) is included so it cannot grow unbounded. + */ +export function buildCacheEvictionTargets( + buildCacheDir: string, + hexCacheDir: string, + coreCacheBuildPath: string, +): string[] { + return [hexCacheDir, join(buildCacheDir, "binaries"), coreCacheBuildPath]; +} + /** * LRU cleanup of build cache directories, debounced via marker file. */ diff --git a/server/services/workers/compile-worker.ts b/server/services/workers/compile-worker.ts index e90837112..427ced5d4 100644 --- a/server/services/workers/compile-worker.ts +++ b/server/services/workers/compile-worker.ts @@ -32,6 +32,7 @@ import { buildSketchHash, checkBinaryExists, checkFileExists, + buildCacheEvictionTargets, cleanupCacheLru, ensureDirectories, execArduinoCliJson, @@ -170,7 +171,7 @@ async function buildCoreFingerprint(task: CompileRequestPayload, fqbn: string): } async function cleanupCacheLruLocal(): Promise { - await cleanupCacheLru(BUILD_CACHE_DIR, [HEX_CACHE_DIR, CORE_CACHE_BUILD_PATH], buildCacheMaxBytes); + await cleanupCacheLru(BUILD_CACHE_DIR, buildCacheEvictionTargets(BUILD_CACHE_DIR, HEX_CACHE_DIR, CORE_CACHE_BUILD_PATH), buildCacheMaxBytes); } async function acquireCoreCache(coreReadyMarker: string, coreLockPath: string, coreFingerprint: string): Promise<{ coreCacheWarm: boolean; acquiredCoreLock: boolean; activeBuildCachePath: string }> { diff --git a/tests/server/services/compiler/cache-manager.test.ts b/tests/server/services/compiler/cache-manager.test.ts index 799ab6140..eca866946 100644 --- a/tests/server/services/compiler/cache-manager.test.ts +++ b/tests/server/services/compiler/cache-manager.test.ts @@ -242,6 +242,20 @@ describe("cache-manager cache cleanup", () => { await expect(readFile(paths[2])).resolves.toEqual(Buffer.alloc(3, 3)); }); + it("removes the output sidecar together with an evicted HEX entry", async () => { + const hexCacheDir = await createHexCacheDirectory(); + const oldHex = join(hexCacheDir, "old.hex"); + await writeFile(oldHex, Buffer.alloc(4, 1)); + await writeFile(join(hexCacheDir, "old.output.txt"), "old output", "utf8"); + await writeFile(join(hexCacheDir, "new.hex"), Buffer.alloc(4, 2)); + await writeFile(join(hexCacheDir, "new.output.txt"), "new output", "utf8"); + await utimes(oldHex, new Date("2020-01-01T00:00:00Z"), new Date("2020-01-01T00:00:00Z")); + + await runHexCacheCleanup(hexCacheDir, 4); + + await expect(readdir(hexCacheDir)).resolves.toEqual(["new.hex", "new.output.txt"]); + }); + it("does not remove HEX entries or sidecars when the cache is within the limit", async () => { const hexCacheDir = await createHexCacheDirectory(); await writeFile(join(hexCacheDir, "sketch.hex"), Buffer.alloc(4, 1)); diff --git a/tests/server/workers/compile-worker-utils.test.ts b/tests/server/workers/compile-worker-utils.test.ts index bd507ab63..7e0eaec1c 100644 --- a/tests/server/workers/compile-worker-utils.test.ts +++ b/tests/server/workers/compile-worker-utils.test.ts @@ -10,6 +10,7 @@ import { acquireCoreCacheLock, collectDirectoryRecords, evictLruEntries, + buildCacheEvictionTargets, cleanupCacheLru, ensureDirectories, execArduinoCliJson, @@ -390,3 +391,13 @@ describe("execArduinoCliJson", () => { expect(result).toBeNull(); }); }); + +describe("buildCacheEvictionTargets", () => { + it("covers the instant-binary store written by ArduinoCompiler", () => { + expect(buildCacheEvictionTargets("/cache", "/cache/hex-cache", "/core/build-cache")).toEqual([ + "/cache/hex-cache", + "/cache/binaries", + "/core/build-cache", + ]); + }); +});