Skip to content

Commit 3de7962

Browse files
conico974Nicolas Dorseuil
andauthored
Add option to persist missing tags in regional cache (#825)
* add an option to persist missing tags * Changeset * prettier fix --------- Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent c202302 commit 3de7962

File tree

4 files changed

+189
-5
lines changed

4 files changed

+189
-5
lines changed

.changeset/loud-mice-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
Add an option to persist missing tags in the regional tag cache

examples/e2e/app-router/open-next.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default defineCloudflareConfig({
1010
// With such a configuration, we could have up to 12 * (8 + 2) = 120 Durable Objects instances
1111
tagCache: shardedTagCache({
1212
baseShardSize: 12,
13+
regionalCache: true,
14+
regionalCacheDangerouslyPersistMissingTags: true,
1315
shardReplication: {
1416
numberOfSoftReplicas: 8,
1517
numberOfHardReplicas: 2,

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.spec.ts

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import shardedDOTagCache, { AVAILABLE_REGIONS, DOId } from "./do-sharded-tag-cac
55
const hasBeenRevalidatedMock = vi.fn();
66
const writeTagsMock = vi.fn();
77
const idFromNameMock = vi.fn();
8-
const getMock = vi
9-
.fn()
10-
.mockReturnValue({ hasBeenRevalidated: hasBeenRevalidatedMock, writeTags: writeTagsMock });
8+
const getRevalidationTimesMock = vi.fn();
9+
const getMock = vi.fn().mockReturnValue({
10+
hasBeenRevalidated: hasBeenRevalidatedMock,
11+
writeTags: writeTagsMock,
12+
getRevalidationTimes: getRevalidationTimesMock,
13+
});
1114
const waitUntilMock = vi.fn().mockImplementation(async (fn) => fn());
1215
globalThis.continent = undefined;
1316
const sendDLQMock = vi.fn();
@@ -391,6 +394,165 @@ describe("DOShardedTagCache", () => {
391394
});
392395
});
393396

397+
describe("putToRegionalCache", () => {
398+
it("should return early if regional cache is disabled", async () => {
399+
const cache = shardedDOTagCache();
400+
const doId = new DOId({
401+
baseShardId: "shard-1",
402+
numberOfReplicas: 1,
403+
shardType: "hard",
404+
});
405+
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
406+
expect(getRevalidationTimesMock).not.toHaveBeenCalled();
407+
});
408+
409+
it("should put the tags in the regional cache if the tags exists in the DO", async () => {
410+
const putMock = vi.fn();
411+
// @ts-expect-error - Defined on cloudfare context
412+
globalThis.caches = {
413+
open: vi.fn().mockResolvedValue({
414+
put: putMock,
415+
}),
416+
};
417+
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
418+
const doId = new DOId({
419+
baseShardId: "shard-1",
420+
numberOfReplicas: 1,
421+
shardType: "hard",
422+
});
423+
424+
getRevalidationTimesMock.mockResolvedValueOnce({ tag1: 123456 });
425+
426+
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
427+
428+
expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
429+
expect(putMock).toHaveBeenCalledWith(
430+
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
431+
expect.any(Response)
432+
);
433+
// @ts-expect-error - Defined on cloudfare context
434+
globalThis.caches = undefined;
435+
});
436+
437+
it("should not put the tags in the regional cache if the tags does not exists in the DO", async () => {
438+
const putMock = vi.fn();
439+
// @ts-expect-error - Defined on cloudfare context
440+
globalThis.caches = {
441+
open: vi.fn().mockResolvedValue({
442+
put: putMock,
443+
}),
444+
};
445+
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
446+
const doId = new DOId({
447+
baseShardId: "shard-1",
448+
numberOfReplicas: 1,
449+
shardType: "hard",
450+
});
451+
452+
getRevalidationTimesMock.mockResolvedValueOnce({});
453+
454+
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
455+
456+
expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
457+
expect(putMock).not.toHaveBeenCalled();
458+
// @ts-expect-error - Defined on cloudfare context
459+
globalThis.caches = undefined;
460+
});
461+
462+
it("should put multiple tags in the regional cache", async () => {
463+
const putMock = vi.fn();
464+
// @ts-expect-error - Defined on cloudfare context
465+
globalThis.caches = {
466+
open: vi.fn().mockResolvedValue({
467+
put: putMock,
468+
}),
469+
};
470+
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
471+
const doId = new DOId({
472+
baseShardId: "shard-1",
473+
numberOfReplicas: 1,
474+
shardType: "hard",
475+
});
476+
477+
getRevalidationTimesMock.mockResolvedValueOnce({ tag1: 123456, tag2: 654321 });
478+
479+
await cache.putToRegionalCache({ doId, tags: ["tag1", "tag2"] }, getMock());
480+
481+
expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1", "tag2"]);
482+
expect(putMock).toHaveBeenCalledWith(
483+
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
484+
expect.any(Response)
485+
);
486+
expect(putMock).toHaveBeenCalledWith(
487+
"http://local.cache/shard/tag-hard;shard-1?tag=tag2",
488+
expect.any(Response)
489+
);
490+
// @ts-expect-error - Defined on cloudfare context
491+
globalThis.caches = undefined;
492+
});
493+
494+
it("should put missing tag in the regional cache if `regionalCacheDangerouslyPersistMissingTags` is true", async () => {
495+
const putMock = vi.fn();
496+
// @ts-expect-error - Defined on cloudfare context
497+
globalThis.caches = {
498+
open: vi.fn().mockResolvedValue({
499+
put: putMock,
500+
}),
501+
};
502+
const cache = shardedDOTagCache({
503+
baseShardSize: 4,
504+
regionalCache: true,
505+
regionalCacheDangerouslyPersistMissingTags: true,
506+
});
507+
const doId = new DOId({
508+
baseShardId: "shard-1",
509+
numberOfReplicas: 1,
510+
shardType: "hard",
511+
});
512+
513+
getRevalidationTimesMock.mockResolvedValueOnce({});
514+
515+
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
516+
517+
expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
518+
expect(putMock).toHaveBeenCalledWith(
519+
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
520+
expect.any(Response)
521+
);
522+
// @ts-expect-error - Defined on cloudfare context
523+
globalThis.caches = undefined;
524+
});
525+
526+
it("should not put missing tag in the regional cache if `regionalCacheDangerouslyPersistMissingTags` is false", async () => {
527+
const putMock = vi.fn();
528+
// @ts-expect-error - Defined on cloudfare context
529+
globalThis.caches = {
530+
open: vi.fn().mockResolvedValue({
531+
put: putMock,
532+
}),
533+
};
534+
const cache = shardedDOTagCache({
535+
baseShardSize: 4,
536+
regionalCache: true,
537+
regionalCacheDangerouslyPersistMissingTags: false,
538+
});
539+
const doId = new DOId({
540+
baseShardId: "shard-1",
541+
numberOfReplicas: 1,
542+
shardType: "hard",
543+
});
544+
545+
getRevalidationTimesMock.mockResolvedValueOnce({});
546+
547+
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
548+
549+
expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
550+
expect(putMock).not.toHaveBeenCalled();
551+
// @ts-expect-error - Defined on cloudfare context
552+
globalThis.caches = undefined;
553+
});
554+
});
555+
394556
describe("getCacheKey", () => {
395557
it("should return the cache key without the random part", async () => {
396558
const cache = shardedDOTagCache();

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ interface ShardedDOTagCacheOptions {
4949
*/
5050
regionalCacheTtlSec?: number;
5151

52+
/**
53+
* Whether to persist missing tags in the regional cache.
54+
* This is dangerous if you don't invalidate the Cache API when you revalidate tags as you could end up storing stale data in the data cache.
55+
*
56+
* @default false
57+
*/
58+
regionalCacheDangerouslyPersistMissingTags?: boolean;
59+
5260
/**
5361
* Enable shard replication to handle higher load.
5462
*
@@ -465,8 +473,15 @@ class ShardedDOTagCache implements NextModeTagCache {
465473
const tagsLastRevalidated = await stub.getRevalidationTimes(tags);
466474
await Promise.all(
467475
tags.map(async (tag) => {
468-
const lastRevalidated = tagsLastRevalidated[tag];
469-
if (lastRevalidated === undefined) return; // Should we store something in the cache if the tag is not found ?
476+
let lastRevalidated = tagsLastRevalidated[tag];
477+
if (lastRevalidated === undefined) {
478+
if (this.opts.regionalCacheDangerouslyPersistMissingTags) {
479+
lastRevalidated = 0; // If the tag is not found, we set it to 0 as it means it has never been revalidated before.
480+
} else {
481+
debugCache("Tag not found in revalidation times", { tag, optsKey });
482+
return; // If the tag is not found, we skip it
483+
}
484+
}
470485
const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
471486
debugCache("Putting to regional cache", { cacheKey, lastRevalidated });
472487
await cache.put(

0 commit comments

Comments
 (0)