Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions runtime/caches/cacheWriteWorker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Worker thread for cache write operations.
// Offloads SHA1 hashing, buffer combining, and FS writes from the main event loop.

const CACHE_MAX_ENTRY_SIZE = parseInt(
Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
) || 2097152;

const textEncoder = new TextEncoder();

const initializedDirs = new Set<string>();
Expand Down Expand Up @@ -57,6 +61,12 @@ function generateCombinedBuffer(
return buf;
}

function shardedPath(cacheDir: string, key: string): string {
const l1 = key.substring(0, 2);
const l2 = key.substring(2, 4);
return `${cacheDir}/${l1}/${l2}/${key}`;
}

// --- Message handler ---

export interface CacheWriteMessage {
Expand Down Expand Up @@ -85,8 +95,12 @@ self.onmessage = async (e: MessageEvent<CacheWriteMessage>) => {
// Combine into single buffer
const buffer = generateCombinedBuffer(body, headersBytes);

// Write to filesystem
const filePath = `${cacheDir}/${cacheKey}`;
if (buffer.length > CACHE_MAX_ENTRY_SIZE) return;

// Write to filesystem (with hex sharding for directory distribution)
const filePath = shardedPath(cacheDir, cacheKey);
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
ensureCacheDir(dir);
await Deno.writeFile(filePath, buffer);
} catch (err) {
console.error("[cache-write-worker] error:", err);
Expand Down
67 changes: 62 additions & 5 deletions runtime/caches/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,45 @@ import {
const FILE_SYSTEM_CACHE_DIRECTORY =
Deno.env.get("FILE_SYSTEM_CACHE_DIRECTORY") ?? "/tmp/deco_cache";

const CACHE_MAX_ENTRY_SIZE = parseInt(
Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
) || 2097152;

// Warn when write rate exceeds this many writes per minute.
// High write rates usually indicate bots, missing cache keys, or very short TTLs.
const CACHE_WRITE_RATE_WARN = parseInt(
Deno.env.get("CACHE_WRITE_RATE_WARN") ?? "500",
) || 500;

// --- Write rate tracking ---
let writeCount = 0;
let writeWindowStart = Date.now();

function trackWriteRate(key: string) {
const now = Date.now();
if (now - writeWindowStart > 60_000) {
writeWindowStart = now;
writeCount = 0;
}
writeCount++;
if (writeCount === CACHE_WRITE_RATE_WARN) {
logger.warn(
`fs_cache: high write rate — ${writeCount} writes in the last minute. ` +
`Latest key: ${key}. ` +
`Consider increasing CACHE_MAX_AGE_S or reviewing loader cacheKey functions. ` +
`Adjust threshold with CACHE_WRITE_RATE_WARN (current: ${CACHE_WRITE_RATE_WARN}/min).`,
);
}
}

const initializedShardDirs = new Set<string>();

function shardedPath(cacheDir: string, key: string): string {
const l1 = key.substring(0, 2);
const l2 = key.substring(2, 4);
return `${cacheDir}/${l1}/${l2}/${key}`;
}

// Reuse TextEncoder instance to avoid repeated instantiation
const textEncoder = new TextEncoder();

Expand Down Expand Up @@ -106,7 +145,7 @@ function createFileSystemCache(): CacheStorage {
if (
FILE_SYSTEM_CACHE_DIRECTORY && !existsSync(FILE_SYSTEM_CACHE_DIRECTORY)
) {
await Deno.mkdirSync(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true });
await Deno.mkdir(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true });
}
isCacheInitialized = true;
} catch (err) {
Expand All @@ -118,11 +157,25 @@ function createFileSystemCache(): CacheStorage {
key: string,
responseArray: Uint8Array,
) {
if (responseArray.length > CACHE_MAX_ENTRY_SIZE) {
// Evict any existing entry so stale data doesn't stay pinned on disk.
deleteFile(key).catch(() => {});
return;
}
if (!isCacheInitialized) {
await assertCacheDirectory();
}
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;

trackWriteRate(key);
const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);

@cubic-dev-ai cubic-dev-ai Bot Mar 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Oversized-entry eviction only targets the new sharded path, leaving legacy flat-path cache files undeleted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/caches/fileSystem.ts, line 169:

<comment>Oversized-entry eviction only targets the new sharded path, leaving legacy flat-path cache files undeleted.</comment>

<file context>
@@ -118,11 +157,25 @@ function createFileSystemCache(): CacheStorage {
-    const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
-
+    trackWriteRate(key);
+    const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
+    const dir = filePath.substring(0, filePath.lastIndexOf("/"));
+    if (!initializedShardDirs.has(dir)) {
</file context>
Fix with Cubic

const dir = filePath.substring(0, filePath.lastIndexOf("/"));
if (!initializedShardDirs.has(dir)) {
try {
await Deno.mkdir(dir, { recursive: true });
initializedShardDirs.add(dir);
} catch {
// transient failure — don't mark initialized so next write retries mkdir
}
}
await Deno.writeFile(filePath, responseArray);
return;
}
Expand All @@ -132,8 +185,12 @@ function createFileSystemCache(): CacheStorage {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
const fileContent = await Deno.readFile(filePath);
if (fileContent.length > CACHE_MAX_ENTRY_SIZE) {
Deno.remove(filePath).catch(() => {});
return null;
}
return fileContent;
} catch (_err) {
const err = _err as { code?: string };
Expand All @@ -151,7 +208,7 @@ function createFileSystemCache(): CacheStorage {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
await Deno.remove(filePath);
return true;
} catch (err) {
Expand Down
Loading