Spaces:
Running
Running
Disk-cache snapshot sidecars to skip cold-start re-downloads
Browse filesNext.js' built-in fetch cache rejects items over 2 MB so the 47 MB
comparison-index, 6 MB peer-ranks, and 2.5 MB hierarchy were re-fetched
from HuggingFace on every cold start. Cache them to a tmpdir
(`SIDECAR_CACHE_DIR`, default `/tmp/eval-card-sidecars`) with a
configurable TTL (`SIDECAR_CACHE_TTL_SECONDS`, default 1 h). Warm
containers now read from disk in sub-second time instead of
re-downloading the snapshot sidecars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- lib/sidecars.ts +64 -3
lib/sidecars.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
| 1 |
import "server-only"
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import type {
|
| 4 |
BackendManifest,
|
| 5 |
ComparisonIndex,
|
|
@@ -30,21 +35,77 @@ function sidecarUrl(name: string) {
|
|
| 30 |
return `${getSnapshotUrl()}/${name}`
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
async function fetchJson<T>(name: string): Promise<T> {
|
| 34 |
const url = sidecarUrl(name)
|
| 35 |
|
| 36 |
if (url.startsWith("file://")) {
|
| 37 |
-
const
|
| 38 |
-
const text = await fs.readFile(new URL(url), "utf8")
|
| 39 |
return JSON.parse(text) as T
|
| 40 |
}
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const response = await fetch(url, { next: { revalidate: 3600 } })
|
| 43 |
if (!response.ok) {
|
| 44 |
throw new Error(`Snapshot sidecar fetch failed: ${response.status} ${response.statusText} for ${url}`)
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
export function fetchManifest(): Promise<BackendManifest> {
|
|
|
|
| 1 |
import "server-only"
|
| 2 |
|
| 3 |
+
import { createHash } from "node:crypto"
|
| 4 |
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises"
|
| 5 |
+
import { tmpdir } from "node:os"
|
| 6 |
+
import { join } from "node:path"
|
| 7 |
+
|
| 8 |
import type {
|
| 9 |
BackendManifest,
|
| 10 |
ComparisonIndex,
|
|
|
|
| 35 |
return `${getSnapshotUrl()}/${name}`
|
| 36 |
}
|
| 37 |
|
| 38 |
+
// Disk cache directory + TTL for the multi-MB sidecar payloads. Next.js'
|
| 39 |
+
// built-in fetch cache rejects items over 2 MB so the 47 MB
|
| 40 |
+
// comparison-index / 6 MB peer-ranks / 2.5 MB hierarchy were re-fetched
|
| 41 |
+
// from HuggingFace on every cold start. With the disk cache, a warm
|
| 42 |
+
// container reads from `/tmp/eval-card-sidecars/*` (sub-second) instead
|
| 43 |
+
// of re-downloading. Override the directory via `SIDECAR_CACHE_DIR` and
|
| 44 |
+
// the TTL via `SIDECAR_CACHE_TTL_SECONDS`.
|
| 45 |
+
const DISK_CACHE_DIR =
|
| 46 |
+
process.env.SIDECAR_CACHE_DIR?.trim() || join(tmpdir(), "eval-card-sidecars")
|
| 47 |
+
const DISK_CACHE_TTL_MS =
|
| 48 |
+
Number.parseInt(process.env.SIDECAR_CACHE_TTL_SECONDS ?? "3600", 10) * 1000
|
| 49 |
+
|
| 50 |
+
function diskCachePath(url: string): string {
|
| 51 |
+
// The path encodes the URL hash so swapping SNAPSHOT_URL doesn't
|
| 52 |
+
// collide with the previous snapshot's cached payloads.
|
| 53 |
+
const hash = createHash("sha1").update(url).digest("hex").slice(0, 16)
|
| 54 |
+
const safeName = url.split("/").slice(-1)[0]?.replace(/[^a-zA-Z0-9._-]/g, "_") ?? "sidecar"
|
| 55 |
+
return join(DISK_CACHE_DIR, `${hash}-${safeName}`)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async function readFromDisk(path: string): Promise<string | null> {
|
| 59 |
+
try {
|
| 60 |
+
const info = await stat(path)
|
| 61 |
+
if (Date.now() - info.mtimeMs > DISK_CACHE_TTL_MS) return null
|
| 62 |
+
return await readFile(path, "utf8")
|
| 63 |
+
} catch {
|
| 64 |
+
return null
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async function writeToDisk(path: string, payload: string): Promise<void> {
|
| 69 |
+
try {
|
| 70 |
+
await mkdir(DISK_CACHE_DIR, { recursive: true })
|
| 71 |
+
// Atomic-ish write: stage to a temp file then rename so concurrent
|
| 72 |
+
// readers never observe a partial payload.
|
| 73 |
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`
|
| 74 |
+
await writeFile(tmpPath, payload, "utf8")
|
| 75 |
+
const fs = await import("node:fs/promises")
|
| 76 |
+
await fs.rename(tmpPath, path)
|
| 77 |
+
} catch (err) {
|
| 78 |
+
// Cache writes are best-effort — log and move on so a read-only FS
|
| 79 |
+
// doesn't break the request.
|
| 80 |
+
console.warn(`[sidecars] failed to write disk cache ${path}: ${err instanceof Error ? err.message : String(err)}`)
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
async function fetchJson<T>(name: string): Promise<T> {
|
| 85 |
const url = sidecarUrl(name)
|
| 86 |
|
| 87 |
if (url.startsWith("file://")) {
|
| 88 |
+
const text = await readFile(new URL(url), "utf8")
|
|
|
|
| 89 |
return JSON.parse(text) as T
|
| 90 |
}
|
| 91 |
|
| 92 |
+
const cachePath = diskCachePath(url)
|
| 93 |
+
const cached = await readFromDisk(cachePath)
|
| 94 |
+
if (cached !== null) {
|
| 95 |
+
return JSON.parse(cached) as T
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
const response = await fetch(url, { next: { revalidate: 3600 } })
|
| 99 |
if (!response.ok) {
|
| 100 |
throw new Error(`Snapshot sidecar fetch failed: ${response.status} ${response.statusText} for ${url}`)
|
| 101 |
}
|
| 102 |
|
| 103 |
+
const text = await response.text()
|
| 104 |
+
// Fire-and-forget: we don't want disk I/O on the hot path for the
|
| 105 |
+
// first request, but we do want subsequent requests in the same
|
| 106 |
+
// container to read from disk.
|
| 107 |
+
void writeToDisk(cachePath, text)
|
| 108 |
+
return JSON.parse(text) as T
|
| 109 |
}
|
| 110 |
|
| 111 |
export function fetchManifest(): Promise<BackendManifest> {
|