Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-mice-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

Add an option to persist missing tags in the regional tag cache
2 changes: 2 additions & 0 deletions examples/e2e/app-router/open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default defineCloudflareConfig({
// With such a configuration, we could have up to 12 * (8 + 2) = 120 Durable Objects instances
tagCache: shardedTagCache({
baseShardSize: 12,
regionalCache: true,
regionalCacheDangerouslyPersistMissingTags: true,
shardReplication: {
numberOfSoftReplicas: 8,
numberOfHardReplicas: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import shardedDOTagCache, { AVAILABLE_REGIONS, DOId } from "./do-sharded-tag-cac
const hasBeenRevalidatedMock = vi.fn();
const writeTagsMock = vi.fn();
const idFromNameMock = vi.fn();
const getMock = vi
.fn()
.mockReturnValue({ hasBeenRevalidated: hasBeenRevalidatedMock, writeTags: writeTagsMock });
const getRevalidationTimesMock = vi.fn();
const getMock = vi.fn().mockReturnValue({
hasBeenRevalidated: hasBeenRevalidatedMock,
writeTags: writeTagsMock,
getRevalidationTimes: getRevalidationTimesMock,
});
const waitUntilMock = vi.fn().mockImplementation(async (fn) => fn());
globalThis.continent = undefined;
const sendDLQMock = vi.fn();
Expand Down Expand Up @@ -391,6 +394,165 @@ describe("DOShardedTagCache", () => {
});
});

describe("putToRegionalCache", () => {
it("should return early if regional cache is disabled", async () => {
const cache = shardedDOTagCache();
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});
await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());
expect(getRevalidationTimesMock).not.toHaveBeenCalled();
});

it("should put the tags in the regional cache if the tags exists in the DO", async () => {
const putMock = vi.fn();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = {
open: vi.fn().mockResolvedValue({
put: putMock,
}),
};
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});

getRevalidationTimesMock.mockResolvedValueOnce({ tag1: 123456 });

await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());

expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
expect(putMock).toHaveBeenCalledWith(
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
expect.any(Response)
);
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = undefined;
});

it("should not put the tags in the regional cache if the tags does not exists in the DO", async () => {
const putMock = vi.fn();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = {
open: vi.fn().mockResolvedValue({
put: putMock,
}),
};
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});

getRevalidationTimesMock.mockResolvedValueOnce({});

await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());

expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
expect(putMock).not.toHaveBeenCalled();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = undefined;
});

it("should put multiple tags in the regional cache", async () => {
const putMock = vi.fn();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = {
open: vi.fn().mockResolvedValue({
put: putMock,
}),
};
const cache = shardedDOTagCache({ baseShardSize: 4, regionalCache: true });
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});

getRevalidationTimesMock.mockResolvedValueOnce({ tag1: 123456, tag2: 654321 });

await cache.putToRegionalCache({ doId, tags: ["tag1", "tag2"] }, getMock());

expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1", "tag2"]);
expect(putMock).toHaveBeenCalledWith(
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
expect.any(Response)
);
expect(putMock).toHaveBeenCalledWith(
"http://local.cache/shard/tag-hard;shard-1?tag=tag2",
expect.any(Response)
);
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = undefined;
});

it("should put missing tag in the regional cache if `regionalCacheDangerouslyPersistMissingTags` is true", async () => {
const putMock = vi.fn();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = {
open: vi.fn().mockResolvedValue({
put: putMock,
}),
};
const cache = shardedDOTagCache({
baseShardSize: 4,
regionalCache: true,
regionalCacheDangerouslyPersistMissingTags: true,
});
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});

getRevalidationTimesMock.mockResolvedValueOnce({});

await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());

expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
expect(putMock).toHaveBeenCalledWith(
"http://local.cache/shard/tag-hard;shard-1?tag=tag1",
expect.any(Response)
);
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = undefined;
});

it("should not put missing tag in the regional cache if `regionalCacheDangerouslyPersistMissingTags` is false", async () => {
const putMock = vi.fn();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = {
open: vi.fn().mockResolvedValue({
put: putMock,
}),
};
const cache = shardedDOTagCache({
baseShardSize: 4,
regionalCache: true,
regionalCacheDangerouslyPersistMissingTags: false,
});
const doId = new DOId({
baseShardId: "shard-1",
numberOfReplicas: 1,
shardType: "hard",
});

getRevalidationTimesMock.mockResolvedValueOnce({});

await cache.putToRegionalCache({ doId, tags: ["tag1"] }, getMock());

expect(getRevalidationTimesMock).toHaveBeenCalledWith(["tag1"]);
expect(putMock).not.toHaveBeenCalled();
// @ts-expect-error - Defined on cloudfare context
globalThis.caches = undefined;
});
});

describe("getCacheKey", () => {
it("should return the cache key without the random part", async () => {
const cache = shardedDOTagCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ interface ShardedDOTagCacheOptions {
*/
regionalCacheTtlSec?: number;

/**
* Whether to persist missing tags in the regional cache.
* 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.
*
* @default false
*/
regionalCacheDangerouslyPersistMissingTags?: boolean;

/**
* Enable shard replication to handle higher load.
*
Expand Down Expand Up @@ -465,8 +473,15 @@ class ShardedDOTagCache implements NextModeTagCache {
const tagsLastRevalidated = await stub.getRevalidationTimes(tags);
await Promise.all(
tags.map(async (tag) => {
const lastRevalidated = tagsLastRevalidated[tag];
if (lastRevalidated === undefined) return; // Should we store something in the cache if the tag is not found ?
let lastRevalidated = tagsLastRevalidated[tag];
if (lastRevalidated === undefined) {
if (this.opts.regionalCacheDangerouslyPersistMissingTags) {
lastRevalidated = 0; // If the tag is not found, we set it to 0 as it means it has never been revalidated before.
} else {
debugCache("Tag not found in revalidation times", { tag, optsKey });
return; // If the tag is not found, we skip it
}
}
const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
debugCache("Putting to regional cache", { cacheKey, lastRevalidated });
await cache.put(
Expand Down