All files / server/services temp-artifact-sweep.ts

96.42% Statements 27/28
91.66% Branches 11/12
100% Functions 7/7
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67          22x       108x         9x                                   4x 30x 4x 4x 4x 1x 11x   4x       13x 13x   9x         9x 9x 41x 39x 39x 39x 37x 37x         9x    
import { readdir, rm, stat } from "node:fs/promises";
import { join } from "node:path";
import { getFastTmpBaseDir } from "../../shared/utils/temp-paths.ts";
 
/** Abandoned temp artifacts are removed once they are older than this. */
export const TEMP_ARTIFACT_MAX_AGE_MS = 5 * 60 * 1000;
 
/** Sketch directories of simulation runs; finished ones are renamed to `<id>.cleanup`. */
export function sandboxTempDir(): string {
  return join(getFastTmpBaseDir(), "unosim-temp");
}
 
/** Per-worker directories of the compile workers. */
export function workerBuildRoot(): string {
  return join(getFastTmpBaseDir(), "unosim-worker-build");
}
 
interface SweepOptions {
  /** The process-relative temp directory swept since before the shared temp directory existed. */
  legacyTempDir: string;
  now?: number;
  maxAgeMs?: number;
}
 
/**
 * Removes temp artifacts that no run or compile uses any more, in the
 * directories that sandbox runs and compile workers actually write to:
 * `.cleanup` directories and `.cleanup.json` files of finished runs, and
 * per-sketch build directories that a compile left in `build-output`.
 * Returns the number of removed entries; never throws.
 */
export async function sweepTempArtifacts(options: SweepOptions): Promise<number> {
  const cutoff = (options.now ?? Date.now()) - (options.maxAgeMs ?? TEMP_ARTIFACT_MAX_AGE_MS);
  const isCleanupItem = (name: string) => name.endsWith(".cleanup") || name.endsWith(".cleanup.json");
  let removed = await removeStaleEntries(options.legacyTempDir, cutoff, isCleanupItem);
  removed += await removeStaleEntries(sandboxTempDir(), cutoff, isCleanupItem);
  for (const worker of await listEntries(workerBuildRoot())) {
    Iif (!worker.startsWith("worker_")) continue;
    removed += await removeStaleEntries(join(workerBuildRoot(), worker, "build-output"), cutoff, () => true);
  }
  return removed;
}
 
async function listEntries(dir: string): Promise<string[]> {
  try {
    return await readdir(dir);
  } catch {
    return [];
  }
}
 
async function removeStaleEntries(dir: string, cutoff: number, matches: (name: string) => boolean): Promise<number> {
  let removed = 0;
  for (const name of await listEntries(dir)) {
    if (!matches(name)) continue;
    const path = join(dir, name);
    try {
      if ((await stat(path)).mtimeMs >= cutoff) continue;
      await rm(path, { recursive: true, force: true });
      removed++;
    } catch {
      // Removed concurrently or not accessible; the next sweep retries.
    }
  }
  return removed;
}