Skip to content

Commit 416af75

Browse files
conico974Nicolas Dorseuil
andauthored
Optimize tag writing process (opennextjs#911)
* only call tag cache once * write tags right away instead of at the end of the request * read from pending write in composable cache * fix unit test * review fix --------- Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent f569a34 commit 416af75

File tree

7 files changed

+72
-14
lines changed

7 files changed

+72
-14
lines changed

packages/open-next/src/adapters/cache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
IncrementalCacheContext,
44
IncrementalCacheValue,
55
} from "types/cache";
6-
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
6+
import { getTagsFromValue, hasBeenRevalidated, writeTags } from "utils/cache";
77
import { isBinaryContentType } from "../utils/binary";
88
import { debug, error, warn } from "./logger";
99

@@ -326,7 +326,7 @@ export default class Cache {
326326
if (globalThis.tagCache.mode === "nextMode") {
327327
const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
328328

329-
await globalThis.tagCache.writeTags(_tags);
329+
await writeTags(_tags);
330330
if (paths.length > 0) {
331331
// TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
332332
// It also means that we'll need to provide the tags used in every request to the wrapper or converter.
@@ -378,7 +378,7 @@ export default class Cache {
378378
}
379379

380380
// Update all keys with the given tag with revalidatedAt set to now
381-
await globalThis.tagCache.writeTags(toInsert);
381+
await writeTags(toInsert);
382382

383383
// We can now invalidate all paths in the CDN
384384
// This only applies to `revalidateTag`, not to `res.revalidate()`
@@ -442,7 +442,7 @@ export default class Cache {
442442
const storedTags = await globalThis.tagCache.getByPath(key);
443443
const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag));
444444
if (tagsToWrite.length > 0) {
445-
await globalThis.tagCache.writeTags(
445+
await writeTags(
446446
tagsToWrite.map((tag) => ({
447447
path: key,
448448
tag: tag,

packages/open-next/src/adapters/composable-cache.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import type { ComposableCacheEntry, ComposableCacheHandler } from "types/cache";
2+
import { writeTags } from "utils/cache";
23
import { fromReadableStream, toReadableStream } from "utils/stream";
34
import { debug } from "./logger";
45

6+
const pendingWritePromiseMap = new Map<string, Promise<ComposableCacheEntry>>();
7+
58
export default {
69
async get(cacheKey: string) {
710
try {
11+
// We first check if we have a pending write for this cache key
12+
// If we do, we return the pending promise instead of fetching the cache
13+
if (pendingWritePromiseMap.has(cacheKey)) {
14+
return pendingWritePromiseMap.get(cacheKey);
15+
}
816
const result = await globalThis.incrementalCache.get(
917
cacheKey,
1018
"composable",
@@ -48,7 +56,10 @@ export default {
4856
},
4957

5058
async set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>) {
51-
const entry = await pendingEntry;
59+
pendingWritePromiseMap.set(cacheKey, pendingEntry);
60+
const entry = await pendingEntry.finally(() => {
61+
pendingWritePromiseMap.delete(cacheKey);
62+
});
5263
const valueToStore = await fromReadableStream(entry.value);
5364
await globalThis.incrementalCache.set(
5465
cacheKey,
@@ -62,9 +73,7 @@ export default {
6273
const storedTags = await globalThis.tagCache.getByPath(cacheKey);
6374
const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
6475
if (tagsToWrite.length > 0) {
65-
await globalThis.tagCache.writeTags(
66-
tagsToWrite.map((tag) => ({ tag, path: cacheKey })),
67-
);
76+
await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
6877
}
6978
}
7079
},
@@ -83,7 +92,7 @@ export default {
8392
},
8493
async expireTags(...tags: string[]) {
8594
if (globalThis.tagCache.mode === "nextMode") {
86-
return globalThis.tagCache.writeTags(tags);
95+
return writeTags(tags);
8796
}
8897
const tagCache = globalThis.tagCache;
8998
const revalidatedAt = Date.now();
@@ -104,7 +113,7 @@ export default {
104113
for (const entry of pathsToUpdate.flat()) {
105114
setToWrite.add(entry);
106115
}
107-
await globalThis.tagCache.writeTags(Array.from(setToWrite));
116+
await writeTags(Array.from(setToWrite));
108117
},
109118

110119
// This one is necessary for older versions of next

packages/open-next/src/types/global.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ interface OpenNextRequestContext {
6363
// Last modified time of the page (used in main functions, only available for ISR/SSG).
6464
lastModified?: number;
6565
waitUntil?: WaitUntil;
66+
/** We use this to deduplicate write of the tags*/
67+
writtenTags: Set<string>;
6668
}
6769

6870
declare global {

packages/open-next/src/types/overrides.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ export type NextModeTagCache = BaseTagCache & {
144144
getPathsByTags?: (tags: string[]) => Promise<string[]>;
145145
};
146146

147+
export interface OriginalTagCacheWriteInput {
148+
tag: string;
149+
path: string;
150+
revalidatedAt?: number;
151+
}
152+
147153
/**
148154
* On get :
149155
We just check for the cache key in the tag cache. If it has been revalidated we just return null, otherwise we continue
@@ -168,9 +174,7 @@ export type OriginalTagCache = BaseTagCache & {
168174
getByTag(tag: string): Promise<string[]>;
169175
getByPath(path: string): Promise<string[]>;
170176
getLastModified(path: string, lastModified?: number): Promise<number>;
171-
writeTags(
172-
tags: { tag: string; path: string; revalidatedAt?: number }[],
173-
): Promise<void>;
177+
writeTags(tags: OriginalTagCacheWriteInput[]): Promise<void>;
174178
};
175179

176180
export type TagCache = NextModeTagCache | OriginalTagCache;

packages/open-next/src/utils/cache.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { CacheValue, WithLastModified } from "types/overrides";
1+
import type {
2+
CacheValue,
3+
OriginalTagCacheWriteInput,
4+
WithLastModified,
5+
} from "types/overrides";
6+
import { debug } from "../adapters/logger";
27

38
export async function hasBeenRevalidated(
49
key: string,
@@ -39,3 +44,39 @@ export function getTagsFromValue(value?: CacheValue<"cache">) {
3944
return [];
4045
}
4146
}
47+
48+
function getTagKey(tag: string | OriginalTagCacheWriteInput): string {
49+
if (typeof tag === "string") {
50+
return tag;
51+
}
52+
return JSON.stringify({
53+
tag: tag.tag,
54+
path: tag.path,
55+
});
56+
}
57+
58+
export async function writeTags(
59+
tags: (string | OriginalTagCacheWriteInput)[],
60+
): Promise<void> {
61+
const store = globalThis.__openNextAls.getStore();
62+
debug("Writing tags", tags, store);
63+
if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) {
64+
return;
65+
}
66+
const tagsToWrite = tags.filter((t) => {
67+
const tagKey = getTagKey(t);
68+
const shouldWrite = !store.writtenTags.has(tagKey);
69+
// We preemptively add the tag to the writtenTags set
70+
// to avoid writing the same tag multiple times in the same request
71+
if (shouldWrite) {
72+
store.writtenTags.add(tagKey);
73+
}
74+
return shouldWrite;
75+
});
76+
if (tagsToWrite.length === 0) {
77+
return;
78+
}
79+
80+
// Here we know that we have the correct type
81+
await globalThis.tagCache.writeTags(tagsToWrite as any);
82+
}

packages/open-next/src/utils/promise.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export function runWithOpenNextRequestContext<T>(
119119
pendingPromiseRunner: new DetachedPromiseRunner(),
120120
isISRRevalidation,
121121
waitUntil,
122+
writtenTags: new Set<string>(),
122123
},
123124
async () => {
124125
provideNextAfterProvider();

packages/tests-unit/tests/adapters/cache.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe("CacheHandler", () => {
5656
resolve: vi.fn(),
5757
}),
5858
},
59+
writtenTags: new Set(),
5960
}),
6061
};
6162

0 commit comments

Comments
 (0)