Skip to content

Commit 4ee93ff

Browse files
authored
fix(redis-strings): prevent mapping tables deletion before cache purge (#3)
This PR addresses a critical data consistency issue in the delete method where mapping table entries could be removed before the cache was successfully cleared. By reversing the operation order to clear cache after mapping tables are updated, we ensure that even in failure scenarios, the cache can still be revalidated through tags. Previously, concurrent operations in Promise.all could lead to orphaned cache entries with no way to revalidate them via tags, potentially serving stale data indefinitely. This change provides a more robust cache invalidation strategy that prioritizes data consistency over performance.
1 parent ce977bd commit 4ee93ff

File tree

2 files changed

+40
-38
lines changed

2 files changed

+40
-38
lines changed

packages/nextjs-cache-handler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"dist/redis-strings.d.ts"
3030
],
3131
"buffer-string-decorator": [
32-
"buffer-string-decorator.d.ts"
32+
"dist/buffer-string-decorator.d.ts"
3333
]
3434
}
3535
},

packages/nextjs-cache-handler/src/handlers/redis-strings.ts

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default function createHandler({
102102
return;
103103
}
104104

105-
const deleteKeysOperation = client.unlink(
105+
await client.unlink(
106106
getTimeoutRedisCommandOptions(timeoutMs),
107107
keysToDelete,
108108
);
@@ -120,7 +120,6 @@ export default function createHandler({
120120
);
121121

122122
await Promise.all([
123-
deleteKeysOperation,
124123
updateTtlOperation,
125124
updateTagsOperation,
126125
]);
@@ -162,6 +161,11 @@ export default function createHandler({
162161
return;
163162
}
164163

164+
await client.unlink(
165+
getTimeoutRedisCommandOptions(timeoutMs),
166+
keysToDelete,
167+
);
168+
165169
const updateTtlOperation = client.hDel(
166170
{
167171
isolated: true,
@@ -180,13 +184,7 @@ export default function createHandler({
180184
tagsAndTtlToDelete,
181185
);
182186

183-
const deleteKeysOperation = client.unlink(
184-
getTimeoutRedisCommandOptions(timeoutMs),
185-
keysToDelete,
186-
);
187-
188187
await Promise.all([
189-
deleteKeysOperation,
190188
updateTagsOperation,
191189
updateTtlOperation,
192190
]);
@@ -251,6 +249,30 @@ export default function createHandler({
251249
let expireOperation: Promise<boolean> | undefined;
252250
const lifespan = cacheHandlerValue.lifespan;
253251

252+
const setTagsOperation =
253+
cacheHandlerValue.tags.length > 0
254+
? client.hSet(
255+
options,
256+
keyPrefix + sharedTagsKey,
257+
key,
258+
JSON.stringify(cacheHandlerValue.tags),
259+
)
260+
: undefined;
261+
262+
const setSharedTtlOperation = lifespan
263+
? client.hSet(
264+
options,
265+
keyPrefix + sharedTagsTtlKey,
266+
key,
267+
lifespan.expireAt,
268+
)
269+
: undefined;
270+
271+
await Promise.all([
272+
setTagsOperation,
273+
setSharedTtlOperation,
274+
]);
275+
254276
switch (keyExpirationStrategy) {
255277
case "EXAT": {
256278
setOperation = client.set(
@@ -284,31 +306,10 @@ export default function createHandler({
284306
}
285307
}
286308

287-
const setTagsOperation =
288-
cacheHandlerValue.tags.length > 0
289-
? client.hSet(
290-
options,
291-
keyPrefix + sharedTagsKey,
292-
key,
293-
JSON.stringify(cacheHandlerValue.tags),
294-
)
295-
: undefined;
296-
297-
const setSharedTtlOperation = lifespan
298-
? client.hSet(
299-
options,
300-
keyPrefix + sharedTagsTtlKey,
301-
key,
302-
lifespan.expireAt,
303-
)
304-
: undefined;
305-
306-
await Promise.all([
307-
setOperation,
308-
expireOperation,
309-
setTagsOperation,
310-
setSharedTtlOperation,
311-
]);
309+
await Promise.all([
310+
setOperation,
311+
expireOperation,
312+
]);
312313
},
313314
async revalidateTag(tag) {
314315
assertClientIsReady();
@@ -329,11 +330,12 @@ export default function createHandler({
329330
await Promise.all([revalidateTags(tag), revalidateSharedKeys()]);
330331
},
331332
async delete(key) {
333+
await client.unlink(
334+
getTimeoutRedisCommandOptions(timeoutMs),
335+
keyPrefix + key,
336+
);
337+
332338
await Promise.all([
333-
client.unlink(
334-
getTimeoutRedisCommandOptions(timeoutMs),
335-
keyPrefix + key,
336-
),
337339
client.hDel(keyPrefix + sharedTagsKey, key),
338340
client.hDel(keyPrefix + sharedTagsTtlKey, key),
339341
]);

0 commit comments

Comments
 (0)