-
Notifications
You must be signed in to change notification settings - Fork 55
feat(cache): filesystem hardening with hex sharding and size limits #1125
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,19 @@ | ||
| import { LRUCache } from "npm:lru-cache@10.2.0"; | ||
| import { ValueType } from "../../deps.ts"; | ||
| import { logger } from "../../observability/otel/config.ts"; | ||
| import { meter } from "../../observability/otel/metrics.ts"; | ||
|
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. Address pipeline failure: file not formatted. The CI pipeline reports that this file fails 🧰 Tools🪛 GitHub Actions: ci[error] 1-1: deno fmt --check found not formatted file: runtime/caches/lrucache.ts. 🤖 Prompt for AI Agents |
||
| import { | ||
| assertCanBeCached, | ||
| assertNoOptions, | ||
| baseCache, | ||
| createBaseCacheStorage, | ||
| } from "./utils.ts"; | ||
|
|
||
| const lruEvictionCounter = meter.createCounter("lru_cache_eviction", { | ||
| unit: "1", | ||
| valueType: ValueType.DOUBLE, | ||
| }); | ||
|
|
||
| // keep compatible with old variable name | ||
| const CACHE_MAX_SIZE = parseInt( | ||
| Deno.env.get("CACHE_MAX_SIZE") ?? Deno.env.get("MAX_CACHE_SIZE") ?? | ||
|
|
@@ -30,17 +38,61 @@ const cacheOptions = (cache: Cache) => ( | |
| maxSize: CACHE_MAX_SIZE, | ||
| ttlAutopurge: CACHE_TTL_AUTOPURGE, | ||
| ttlResolution: CACHE_TTL_RESOLUTION, | ||
| dispose: async (_value: boolean, key: string) => { | ||
| dispose: async (_value: boolean, key: string, reason: string) => { | ||
| lruEvictionCounter.add(1, { reason }); | ||
| await cache.delete(key); | ||
| }, | ||
|
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. 🧩 Analysis chain🌐 Web query:
💡 Result: The dispose callback in the lru-cache npm package (current version 11.2.7 as of 2026-03-13) has the signature (value: V, key: K, reason: DisposeReason) => void. It is provided via the dispose or disposeAfter options when creating an LRUCache instance. The reason parameter is a string literal union type DisposeReason with the following possible values: - 'evict': Item evicted because it is the least recently used and the cache is full. - 'set': A new value was set, overwriting the old value. - 'delete': Item explicitly deleted via delete, clear, or set with undefined. - 'expire': Item removed due to exceeding its TTL. - 'fetch': A fetchMethod operation returned undefined or was aborted. Example usage: const cache = new LRUCache({ max: 500, dispose: (value, key, reason) => { console.log(Disposing ${key} (${reason}):, value); // cleanup logic here } }); Note: The dispose callback is called before the item is fully removed from the cache. Use disposeAfter for post-removal cleanup. In older versions (< v7), the signature was (key, value) without reason. Citations:
Fix the The lru-cache library (v10.2.0 and later) expects the 🤖 Prompt for AI Agents |
||
| } | ||
| ); | ||
|
|
||
| const lruSizeGauge = meter.createObservableGauge("lru_cache_keys", { | ||
| description: "number of keys in the LRU cache", | ||
| unit: "1", | ||
| valueType: ValueType.DOUBLE, | ||
| }); | ||
|
|
||
| const lruBytesGauge = meter.createObservableGauge("lru_cache_bytes", { | ||
| description: "total bytes tracked by the LRU cache", | ||
| unit: "bytes", | ||
| valueType: ValueType.DOUBLE, | ||
| }); | ||
|
|
||
| // deno-lint-ignore no-explicit-any | ||
| const activeCaches = new Map<string, LRUCache<string, any>>(); | ||
|
|
||
| lruSizeGauge.addCallback((observer) => { | ||
| for (const [name, lru] of activeCaches) { | ||
| observer.observe(lru.size, { cache: name }); | ||
| } | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Warn when LRU disk usage exceeds this fraction of CACHE_MAX_SIZE. | ||
| // At this point the LRU is evicting aggressively and disk is nearly full. | ||
| const LRU_DISK_WARN_RATIO = parseFloat( | ||
| Deno.env.get("LRU_DISK_WARN_RATIO") ?? "0.9", | ||
| ); | ||
|
|
||
| lruBytesGauge.addCallback((observer) => { | ||
| for (const [name, lru] of activeCaches) { | ||
| observer.observe(lru.calculatedSize, { cache: name }); | ||
| const ratio = lru.calculatedSize / CACHE_MAX_SIZE; | ||
| if (ratio >= LRU_DISK_WARN_RATIO) { | ||
| logger.warn( | ||
| `lru_cache: disk usage for cache "${name}" is at ` + | ||
| `${Math.round(lru.calculatedSize / 1024 / 1024)}MB / ` + | ||
| `${Math.round(CACHE_MAX_SIZE / 1024 / 1024)}MB (${Math.round(ratio * 100)}%). ` + | ||
| `LRU is evicting aggressively. Consider increasing CACHE_MAX_SIZE or reducing CACHE_MAX_AGE_S.`, | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| function createLruCacheStorage(cacheStorageInner: CacheStorage): CacheStorage { | ||
| const caches = createBaseCacheStorage( | ||
| cacheStorageInner, | ||
| (_cacheName, cacheInner, requestURLSHA1) => { | ||
| const fileCache = new LRUCache(cacheOptions(cacheInner)); | ||
| activeCaches.set(_cacheName, fileCache); | ||
| return Promise.resolve({ | ||
| ...baseCache, | ||
| delete: async ( | ||
|
|
||
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: Oversized-entry eviction only targets the new sharded path, leaving legacy flat-path cache files undeleted.
Prompt for AI agents