Skip to content

Commit af2b430

Browse files
committed
add an option to persist missing tags
1 parent c202302 commit af2b430

File tree

3 files changed

+184
-3
lines changed

3 files changed

+184
-3
lines changed

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ 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 getRevalidationTimesMock = vi.fn();
89
const getMock = vi
910
.fn()
10-
.mockReturnValue({ hasBeenRevalidated: hasBeenRevalidatedMock, writeTags: writeTagsMock });
11+
.mockReturnValue({
12+
hasBeenRevalidated: hasBeenRevalidatedMock,
13+
writeTags: writeTagsMock,
14+
getRevalidationTimes: getRevalidationTimesMock,
15+
});
1116
const waitUntilMock = vi.fn().mockImplementation(async (fn) => fn());
1217
globalThis.continent = undefined;
1318
const sendDLQMock = vi.fn();
@@ -391,6 +396,165 @@ describe("DOShardedTagCache", () => {
391396
});
392397
});
393398

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