-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathlrucache.ts
More file actions
161 lines (146 loc) · 5.79 KB
/
Copy pathlrucache.ts
File metadata and controls
161 lines (146 loc) · 5.79 KB
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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";
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") ??
"1073741824",
); // 1 GB max size of cache
const CACHE_MAX_ITEMS = parseInt(
Deno.env.get("CACHE_MAX_ITEMS") ?? "4096",
); // max number of items in the LRU cache (bounds internal typed array memory)
const CACHE_TTL_AUTOPURGE = Deno.env.get("CACHE_TTL_AUTOPURGE") === "true"; // creates a cron for each element in the cache to automatically delete expired items
const CACHE_TTL_RESOLUTION = parseInt(
Deno.env.get("CACHE_TTL_RESOLUTION") ?? "1000",
); // updates the lru cache timer every 1 second
// How long stale content remains serveable (and stays on disk) beyond its expires header.
// Default: 1 hour — long enough for low-traffic sites to keep serving cached content across
// quiet periods while background revalidation catches up.
const STALE_TTL_PERIOD = parseInt(
Deno.env.get("STALE_TTL_PERIOD") ?? "3600000", // 1h
);
const cacheOptions = (cache: Cache) => (
{
max: CACHE_MAX_ITEMS,
maxSize: CACHE_MAX_SIZE,
ttlAutopurge: CACHE_TTL_AUTOPURGE,
ttlResolution: CACHE_TTL_RESOLUTION,
dispose: async (_value: boolean, key: string, reason: string) => {
lruEvictionCounter.add(1, { reason });
await cache.delete(key);
},
}
);
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 });
}
});
// 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 openedCachesByName = new Map<string, Promise<Cache>>();
const caches = createBaseCacheStorage(
cacheStorageInner,
(_cacheName, cacheInner, requestURLSHA1) => {
const existing = openedCachesByName.get(_cacheName);
if (existing) return existing;
const fileCache = new LRUCache(cacheOptions(cacheInner));
activeCaches.set(_cacheName, fileCache);
const cache = Promise.resolve({
...baseCache,
delete: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<boolean> => {
const cacheKey = await requestURLSHA1(request);
cacheInner.delete(cacheKey, options);
return fileCache.delete(cacheKey);
},
match: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<Response | undefined> => {
assertNoOptions(options);
const cacheKey = await requestURLSHA1(request);
if (fileCache.has(cacheKey)) {
return cacheInner.match(cacheKey);
}
return undefined;
},
put: async (
request: RequestInfo | URL,
response: Response,
): Promise<void> => {
const req = new Request(request);
assertCanBeCached(req, response);
if (!response.body) {
return;
}
const expirationTimestamp = Date.parse(
response.headers.get("expires") ?? "",
);
// Calculate the time-to-live (TTL) for the cached item:
// - If STALE_TTL_PERIOD is configured, add it to the expiration time from the response headers
// This allows extending the cache lifetime beyond what the server specifies and serves stale content during this extra time
// The staleness is detect at the loader level because it checks for the expires header, that remains untouched here, the idea is to serve stale content but with expired header
const ttl = (expirationTimestamp - Date.now()) + STALE_TTL_PERIOD;
const cacheKey = await requestURLSHA1(request);
const length = response.headers.get("Content-Length");
if (!length || length == "0") {
return;
}
fileCache.set(cacheKey, true, {
size: parseInt(length),
ttl,
});
return cacheInner.put(cacheKey, response);
},
});
openedCachesByName.set(_cacheName, cache);
return cache;
},
);
return caches;
}
export const caches = (cache: CacheStorage) => createLruCacheStorage(cache);