-
Notifications
You must be signed in to change notification settings - Fork 55
feat(cache): in-memory tier, size limits, sharding, observability #1122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vibegui
wants to merge
19
commits into
main
Choose a base branch
from
vibegui/loader-cache-layers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
e6d5b28
feat(cache): add in-memory tier, size limits, hex sharding, and obser…
vibegui 8c4e5d1
feat(cache): add comprehensive cache observability metrics
vibegui 9e3c534
fix(cache): preserve response status, validate env vars, fix metric o…
vibegui 7ba9241
fix(cache): address code review findings
vibegui 0010914
fix(cache): address correctness issues in env parsing, shard init, LR…
vibegui 23b5ce1
feat(cache): lazy re-index disk entries into LRU on pod restart
vibegui b3696ff
perf(cache): eliminate hot-path allocations and fix correctness bugs
vibegui 60d5f50
fix(cache): async mkdir and single-tier cache-tier header
vibegui ad86a9b
fix(cache): reject oversized entries before writing to disk
vibegui 7b2e290
test(cache): add tests for tiered cache, in-memory cache, and lazy re…
vibegui 9ed8346
fix(cache): evict stale disk entry when oversized write is rejected
vibegui 464e1f0
fix(cache): NaN guard, oversized check in L1, memoize LRU open()
vibegui 093066e
perf(cache): skip body read in L1 put when Content-Length exceeds limit
vibegui ff09a48
feat(cache): extend default stale window to 1h, add STALE_WINDOW_S en…
vibegui 84560bf
fix(cache): bump STALE_TTL_PERIOD default from 30s to 1h
vibegui 2c062bd
feat(cache): bot write guard, L1 admission filter, eviction logging
vibegui ac7edcd
fix(cache): update inMemoryCache tests for admission filter
vibegui 7f6bbac
feat(cache): write rate warning and disk fill warning
vibegui 9f84eee
fix(cache): separate singleFlight key for bot requests
vibegui File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,18 @@ 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; | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using Prompt for AI agents |
||
|
|
||
| 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(); | ||
|
|
||
|
|
@@ -106,7 +118,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) { | ||
|
|
@@ -118,11 +130,24 @@ 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}`; | ||
|
|
||
| const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key); | ||
| 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; | ||
| } | ||
|
|
@@ -132,8 +157,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 }; | ||
|
|
@@ -151,7 +180,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) { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { assertEquals, assertNotEquals } from "@std/assert"; | ||
| import { caches } from "./inMemoryCache.ts"; | ||
|
|
||
| // Use unique cache names per test to avoid cross-test contamination | ||
| // from the shared singleton LRU store. | ||
| let seq = 0; | ||
| const nextCache = () => `inMemoryTest_${seq++}_${Date.now()}`; | ||
|
|
||
| const REQ = new Request("https://example.com/item"); | ||
|
|
||
| Deno.test("inMemoryCache: preserves response body", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| await cache.put(REQ, new Response("hello world")); | ||
| const result = await cache.match(REQ); | ||
| assertNotEquals(result, undefined); | ||
| assertEquals(await result!.text(), "hello world"); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: preserves response status", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| await cache.put(REQ, new Response("not found", { status: 404 })); | ||
| const result = await cache.match(REQ); | ||
| assertEquals(result?.status, 404); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: preserves non-standard status codes", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| await cache.put(REQ, new Response("gone", { status: 410 })); | ||
| assertEquals((await cache.match(REQ))?.status, 410); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: preserves response headers", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| await cache.put( | ||
| REQ, | ||
| new Response("data", { | ||
| headers: { "content-type": "application/json", "x-custom": "value" }, | ||
| }), | ||
| ); | ||
| const result = await cache.match(REQ); | ||
| assertEquals(result?.headers.get("content-type"), "application/json"); | ||
| assertEquals(result?.headers.get("x-custom"), "value"); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: miss returns undefined", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| assertEquals(await cache.match(REQ), undefined); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: delete removes entry", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| await cache.put(REQ, new Response("data")); | ||
| assertNotEquals(await cache.match(REQ), undefined); | ||
| await cache.delete(REQ); | ||
| assertEquals(await cache.match(REQ), undefined); | ||
| }); | ||
|
|
||
| Deno.test("inMemoryCache: different requests are independent", async () => { | ||
| const cache = await caches.open(nextCache()); | ||
| const req1 = new Request("https://example.com/1"); | ||
| const req2 = new Request("https://example.com/2"); | ||
|
|
||
| await cache.put(req1, new Response("one", { status: 200 })); | ||
| await cache.put(req2, new Response("two", { status: 201 })); | ||
|
|
||
| assertEquals((await cache.match(req1))?.status, 200); | ||
| assertEquals((await cache.match(req2))?.status, 201); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The
|| 2097152fallback makes an explicitCACHE_MAX_ENTRY_SIZE=0impossible, because0is coerced to the default 2MB.Prompt for AI agents