Skip to content

Commit f173e01

Browse files
authored
Use cloudflare regional cache instead of our own implementation. (#3673)
Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent 1b458c4 commit f173e01

File tree

1 file changed

+19
-87
lines changed

1 file changed

+19
-87
lines changed

packages/gitbook/openNext/incrementalCache.ts

Lines changed: 19 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
} from '@opennextjs/aws/types/overrides.js';
88
import { getCloudflareContext } from '@opennextjs/cloudflare';
99

10+
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
11+
1012
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
1113

1214
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
@@ -18,51 +20,37 @@ export type KeyOptions = {
1820

1921
/**
2022
*
21-
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
22-
* the cache operations. It also integrates both R2 and Cache API in a single class.
23-
* Having our own, will allow us to customize it in the future if needed.
23+
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
24+
* R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
2425
*/
2526
class GitbookIncrementalCache implements IncrementalCache {
2627
name = 'GitbookIncrementalCache';
2728

28-
protected localCache: Cache | undefined;
29-
3029
async get<CacheType extends CacheEntryType = 'cache'>(
3130
key: string,
3231
cacheType?: CacheType
3332
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
3433
const cacheKey = this.getR2Key(key, cacheType);
3534

3635
const r2 = getCloudflareContext().env[BINDING_NAME];
37-
const localCache = await this.getCacheInstance();
3836
if (!r2) throw new Error('No R2 bucket');
3937
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
4038
// We are in a local middleware environment, we should bypass the cache
4139
// and go directly to the server.
4240
return null;
4341
}
4442
try {
45-
// Check local cache first if available
46-
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
47-
if (localCacheEntry) {
48-
const result = (await localCacheEntry.json()) as WithLastModified<
49-
CacheValue<CacheType>
50-
>;
51-
return this.returnNullOn404({
52-
...result,
53-
// Because we use tag cache and also invalidate them every time,
54-
// if we get a cache hit, we don't need to check the tag cache as we already know it's not been revalidated
55-
// this should improve performance even further, and reduce costs
56-
shouldBypassTagCache: true,
57-
});
58-
}
59-
6043
const r2Object = await r2.get(cacheKey);
6144
if (!r2Object) return null;
6245

46+
const json = (await r2Object.json()) as CacheValue<CacheType>;
47+
const lastModified = r2Object.uploaded.getTime();
48+
49+
if (!json) return null;
50+
6351
return this.returnNullOn404({
64-
value: await r2Object.json(),
65-
lastModified: r2Object.uploaded.getTime(),
52+
value: json,
53+
lastModified,
6654
});
6755
} catch (e) {
6856
console.error('Failed to get from cache', e);
@@ -89,39 +77,8 @@ class GitbookIncrementalCache implements IncrementalCache {
8977
): Promise<void> {
9078
const cacheKey = this.getR2Key(key, cacheType);
9179

92-
const localCache = await this.getCacheInstance();
93-
9480
try {
9581
await this.writeToR2(cacheKey, JSON.stringify(value));
96-
97-
//TODO: Check if there is any places where we don't have tags
98-
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
99-
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
100-
const tags = this.getTagsFromCacheEntry(value) ?? [
101-
`build_id/${process.env.NEXT_BUILD_ID}`,
102-
];
103-
104-
// We consider R2 as the source of truth, so we update the local cache
105-
// only after a successful R2 write
106-
await localCache.put(
107-
this.getCacheUrlKey(cacheKey),
108-
new Response(
109-
JSON.stringify({
110-
value,
111-
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
112-
// See https://developers.cloudflare.com/workers/reference/security-model/
113-
lastModified: Date.now(),
114-
}),
115-
{
116-
headers: {
117-
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
118-
// In theory we should always get the `revalidate` value
119-
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
120-
'cache-tag': tags.join(','),
121-
},
122-
}
123-
)
124-
);
12582
} catch (e) {
12683
console.error('Failed to set to cache', e);
12784
}
@@ -131,14 +88,10 @@ class GitbookIncrementalCache implements IncrementalCache {
13188
const cacheKey = this.getR2Key(key);
13289

13390
const r2 = getCloudflareContext().env[BINDING_NAME];
134-
const localCache = await this.getCacheInstance();
13591
if (!r2) throw new Error('No R2 bucket');
13692

13793
try {
13894
await r2.delete(cacheKey);
139-
140-
// Here again R2 is the source of truth, so we delete from local cache first
141-
await localCache.delete(this.getCacheUrlKey(cacheKey));
14295
} catch (e) {
14396
console.error('Failed to delete from cache', e);
14497
}
@@ -168,12 +121,6 @@ class GitbookIncrementalCache implements IncrementalCache {
168121
}
169122
}
170123

171-
async getCacheInstance(): Promise<Cache> {
172-
if (this.localCache) return this.localCache;
173-
this.localCache = await caches.open('incremental-cache');
174-
return this.localCache;
175-
}
176-
177124
// Utility function to generate keys for R2/Cache API
178125
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
179126
let key = initialKey;
@@ -189,28 +136,13 @@ class GitbookIncrementalCache implements IncrementalCache {
189136
'/'
190137
);
191138
}
192-
193-
getCacheUrlKey(cacheKey: string): string {
194-
return `http://cache.local/${cacheKey}`;
195-
}
196-
197-
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
198-
entry: CacheValue<CacheType>
199-
): string[] | undefined {
200-
if ('tags' in entry && entry.tags) {
201-
return entry.tags;
202-
}
203-
204-
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
205-
const rawTags = entry.meta.headers['x-next-cache-tags'];
206-
if (typeof rawTags === 'string') {
207-
return rawTags.split(',');
208-
}
209-
}
210-
if ('value' in entry) {
211-
return entry.tags;
212-
}
213-
}
214139
}
215140

216-
export default new GitbookIncrementalCache();
141+
export default withRegionalCache(new GitbookIncrementalCache(), {
142+
mode: 'long-lived',
143+
// We can do it because we use our own logic to invalidate the cache
144+
bypassTagCacheOnCacheHit: true,
145+
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
146+
// We don't want to update the cache entry on every cache hit
147+
shouldLazilyUpdateOnCacheHit: false,
148+
});

0 commit comments

Comments
 (0)