Skip to content

Commit c62c986

Browse files
Refactor Redis Handlers timeout handling (#257)
1 parent a0b23ae commit c62c986

File tree

9 files changed

+69
-58
lines changed

9 files changed

+69
-58
lines changed

.changeset/hip-taxis-listen.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@neshca/cache-handler': patch
3+
---
4+
5+
Refactored Redis Handlers timeout handling
6+
7+
#### Changes
8+
9+
- Refactored Redis Handlers to use `AbortSignal` instead of promisifying `setTimeout`.
10+
- Set default Redis Handlers `timeoutMs` option to 5000 ms.

apps/cache-testing/cache-handler-redis-stack.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@ if (!process.env.REDIS_URL) {
88
console.warn('Make sure that REDIS_URL is added to the .env.local file and loaded properly.');
99
}
1010

11-
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
12-
1311
const client = createClient({
1412
url: process.env.REDIS_URL,
1513
name: `cache-handler:${process.env.PORT ?? process.pid}`,
16-
socket: {
17-
connectTimeout: CONNECT_TIMEOUT_MS,
18-
},
1914
});
2015

2116
client.on('error', (error) => {
@@ -30,9 +25,10 @@ IncrementalCache.onCreation(async () => {
3025
const redisCache = await createRedisCache({
3126
client,
3227
keyPrefix: 'JSON:',
28+
timeoutMs: 1000,
3329
});
3430

35-
const redisStringsCache = createRedisStringsCache({ client, keyPrefix: 'strings:' });
31+
const redisStringsCache = createRedisStringsCache({ client, keyPrefix: 'strings:', timeoutMs: 1000 });
3632

3733
const localCache = createLruCache();
3834

apps/cache-testing/cache-handler-redis-strings.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,22 @@ if (!process.env.REDIS_URL) {
88
}
99

1010
const PREFIX = 'string:';
11-
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000;
1211

1312
const client = createClient({
1413
url: process.env.REDIS_URL,
1514
name: `cache-handler:${PREFIX}${process.env.PORT ?? process.pid}`,
16-
socket: {
17-
connectTimeout: CONNECT_TIMEOUT_MS,
18-
},
1915
});
2016

2117
client.on('error', (error) => {
2218
console.error('Redis error:', error);
2319
});
2420

2521
IncrementalCache.onCreation(async () => {
22+
console.log('Connecting Redis client...');
2623
await client.connect();
24+
console.log('Redis client connected.');
2725

28-
const redisCache = await createRedisCache({
26+
const redisCache = createRedisCache({
2927
client,
3028
keyPrefix: PREFIX,
3129
});

docs/cache-handler-docs/theme.config.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default {
5353
key: '0.6.0-release',
5454
text: (
5555
<a href={`${process.env.NEXT_PUBLIC_BASE_URL}`}>
56-
🎉 Version 0.6.4 is out, offering stale-while-revalidate strategy emulation!
56+
🎉 Version 0.6.5 is out, offering stale-while-revalidate strategy emulation and codebase improvements!
5757
</a>
5858
),
5959
},

docs/contributing/cache-handler.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ npm run e2e:ui -w @neshca/cache-testing
5050
**Important**: Clear the Redis DB before executing tests:
5151
5252
```bash
53-
docker exec -it redis-stack redis-cli
53+
docker exec -it cache-handler-redis redis-cli
5454
127.0.0.1:6379> flushall
5555
OK
5656
```

packages/cache-handler/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Flexible API to replace the default Next.js cache, accommodating custom cache solutions for multi-instance self-hosted deployments**
44

5-
🎉 Version 0.6.4 is out, offering stale-while-revalidate strategy emulation!
5+
🎉 Version 0.6.5 is out, offering stale-while-revalidate strategy emulation and codebase improvements!
66

77
Check out the [changelog](https://github.com/caching-tools/next-shared-cache/blob/canary/packages/cache-handler/CHANGELOG.md)
88

packages/cache-handler/src/handlers/redis-stack.ts

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import type { RedisClientType } from 'redis';
33
import type { RevalidatedTags, CacheHandlerValue, Cache } from '../cache-handler';
44
import type { RedisJSON, UseTtlOptions } from '../common-types';
5-
import { promiseWithTimeout } from '../helpers/promise-with-timeout';
65
import { calculateEvictionDelay } from '../helpers/calculate-eviction-delay';
6+
import { getTimeoutRedisCommandOptions } from '../helpers/get-timeout-redis-command-options';
77

88
/**
99
* The configuration options for the Redis Handler
@@ -22,7 +22,7 @@ export type RedisCacheHandlerOptions<T> = UseTtlOptions & {
2222
*/
2323
revalidatedTagsKey?: string;
2424
/**
25-
* Timeout in milliseconds for Redis operations.
25+
* Timeout in milliseconds for Redis operations. Defaults to 5000.
2626
*/
2727
timeoutMs?: number;
2828
};
@@ -60,7 +60,7 @@ export default async function createCache<T extends RedisClientType>({
6060
keyPrefix = '',
6161
revalidatedTagsKey = '__sharedRevalidatedTags__',
6262
useTtl = false,
63-
timeoutMs,
63+
timeoutMs = 5000,
6464
}: RedisCacheHandlerOptions<T>): Promise<Cache> {
6565
function assertClientIsReady(): void {
6666
if (!client.isReady) {
@@ -70,7 +70,8 @@ export default async function createCache<T extends RedisClientType>({
7070

7171
assertClientIsReady();
7272

73-
const setInitialRevalidatedTags = client.json.set(
73+
await client.json.set(
74+
getTimeoutRedisCommandOptions(timeoutMs),
7475
keyPrefix + revalidatedTagsKey,
7576
'.',
7677
{},
@@ -79,17 +80,15 @@ export default async function createCache<T extends RedisClientType>({
7980
},
8081
);
8182

82-
await promiseWithTimeout(setInitialRevalidatedTags, timeoutMs);
83-
8483
return {
8584
name: 'redis-stack',
8685
async get(key) {
8786
assertClientIsReady();
8887

89-
const getCacheValue = client.json.get(keyPrefix + key);
90-
91-
const cacheValue = ((await promiseWithTimeout(getCacheValue, timeoutMs)) ??
92-
null) as CacheHandlerValue | null;
88+
const cacheValue = (await client.json.get(
89+
getTimeoutRedisCommandOptions(timeoutMs),
90+
keyPrefix + key,
91+
)) as CacheHandlerValue | null;
9392

9493
if (cacheValue?.value?.kind === 'ROUTE') {
9594
cacheValue.value.body = Buffer.from(cacheValue.value.body as unknown as string, 'base64');
@@ -108,41 +107,44 @@ export default async function createCache<T extends RedisClientType>({
108107
preparedCacheValue.value.body = cacheValue.value.body.toString('base64') as unknown as Buffer;
109108
}
110109

111-
const setCacheValue = client.json.set(keyPrefix + key, '.', preparedCacheValue as unknown as RedisJSON);
110+
const options = getTimeoutRedisCommandOptions(timeoutMs);
112111

113-
await promiseWithTimeout(setCacheValue, timeoutMs);
112+
const setCacheValue = client.json.set(
113+
options,
114+
keyPrefix + key,
115+
'.',
116+
preparedCacheValue as unknown as RedisJSON,
117+
);
114118

115-
if (typeof maxAgeSeconds !== 'number') {
116-
return;
117-
}
119+
const commands: Promise<unknown>[] = [setCacheValue];
118120

119121
const evictionDelay = calculateEvictionDelay(maxAgeSeconds, useTtl);
120122

121-
if (!evictionDelay) {
122-
return;
123+
if (evictionDelay) {
124+
commands.push(client.expire(options, keyPrefix + key, evictionDelay));
123125
}
124126

125-
const setExpire = client.expire(keyPrefix + key, evictionDelay);
126-
127-
await promiseWithTimeout(setExpire, timeoutMs);
127+
await Promise.all(commands);
128128
},
129129
async getRevalidatedTags() {
130130
assertClientIsReady();
131131

132-
const getRevalidatedTags = client.json.get(keyPrefix + revalidatedTagsKey);
133-
134-
const sharedRevalidatedTags = ((await promiseWithTimeout(getRevalidatedTags, timeoutMs)) ?? undefined) as
135-
| RevalidatedTags
136-
| undefined;
132+
const sharedRevalidatedTags = (await client.json.get(
133+
getTimeoutRedisCommandOptions(timeoutMs),
134+
keyPrefix + revalidatedTagsKey,
135+
)) as RevalidatedTags | undefined;
137136

138137
return sharedRevalidatedTags;
139138
},
140139
async revalidateTag(tag, revalidatedAt) {
141140
assertClientIsReady();
142141

143-
const setRevalidatedTags = client.json.set(keyPrefix + revalidatedTagsKey, `.${tag}`, revalidatedAt);
144-
145-
await promiseWithTimeout(setRevalidatedTags, timeoutMs);
142+
await client.json.set(
143+
getTimeoutRedisCommandOptions(timeoutMs),
144+
keyPrefix + revalidatedTagsKey,
145+
`.${tag}`,
146+
revalidatedAt,
147+
);
146148
},
147149
};
148150
}

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable import/no-default-export -- use default here */
2-
import { reviveFromBase64Representation, replaceJsonWithBase64 } from '@neshca/json-replacer-reviver';
2+
import { replaceJsonWithBase64, reviveFromBase64Representation } from '@neshca/json-replacer-reviver';
33
import type { RedisClientType } from 'redis';
4-
import { promiseWithTimeout } from '../helpers/promise-with-timeout';
5-
import type { RevalidatedTags, CacheHandlerValue, Cache } from '../cache-handler';
4+
import type { Cache, CacheHandlerValue, RevalidatedTags } from '../cache-handler';
65
import { calculateEvictionDelay } from '../helpers/calculate-eviction-delay';
6+
import { getTimeoutRedisCommandOptions } from '../helpers/get-timeout-redis-command-options';
77
import type { RedisCacheHandlerOptions } from './redis-stack';
88

99
/**
@@ -39,11 +39,11 @@ export default function createCache<T extends RedisClientType>({
3939
keyPrefix = '',
4040
revalidatedTagsKey = '__sharedRevalidatedTags__',
4141
useTtl = false,
42-
timeoutMs,
42+
timeoutMs = 5000,
4343
}: RedisCacheHandlerOptions<T>): Cache {
4444
function assertClientIsReady(): void {
4545
if (!client.isReady) {
46-
throw new Error('Redis client is not ready');
46+
throw new Error('Redis client is not ready yet or connection is lost. Keep trying...');
4747
}
4848
}
4949

@@ -52,9 +52,9 @@ export default function createCache<T extends RedisClientType>({
5252
async get(key) {
5353
assertClientIsReady();
5454

55-
const getOperation = client.get(keyPrefix + key);
55+
const getOperation = client.get(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + key);
5656

57-
const result = await promiseWithTimeout(getOperation, timeoutMs);
57+
const result = await getOperation;
5858

5959
if (!result) {
6060
return null;
@@ -69,20 +69,20 @@ export default function createCache<T extends RedisClientType>({
6969
const evictionDelay = calculateEvictionDelay(maxAgeSeconds, useTtl);
7070

7171
// use replaceJsonWithBase64 to store binary data in Base64 and save space
72-
const setOperation = client.set(
72+
await client.set(
73+
getTimeoutRedisCommandOptions(timeoutMs),
7374
keyPrefix + key,
7475
JSON.stringify(value, replaceJsonWithBase64),
7576
evictionDelay ? { EX: evictionDelay } : undefined,
7677
);
77-
78-
await promiseWithTimeout(setOperation, timeoutMs);
7978
},
8079
async getRevalidatedTags() {
8180
assertClientIsReady();
8281

83-
const getOperation = client.hGetAll(keyPrefix + revalidatedTagsKey);
84-
85-
const sharedRevalidatedTags = await promiseWithTimeout(getOperation, timeoutMs);
82+
const sharedRevalidatedTags = await client.hGetAll(
83+
getTimeoutRedisCommandOptions(timeoutMs),
84+
keyPrefix + revalidatedTagsKey,
85+
);
8686

8787
const entries = Object.entries(sharedRevalidatedTags);
8888

@@ -97,11 +97,9 @@ export default function createCache<T extends RedisClientType>({
9797
async revalidateTag(tag, revalidatedAt) {
9898
assertClientIsReady();
9999

100-
const setOperation = client.hSet(keyPrefix + revalidatedTagsKey, {
100+
await client.hSet(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + revalidatedTagsKey, {
101101
[tag]: revalidatedAt,
102102
});
103-
104-
await promiseWithTimeout(setOperation, timeoutMs);
105103
},
106104
};
107105
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { commandOptions } from 'redis';
2+
3+
type CommandOptions = ReturnType<typeof commandOptions>;
4+
5+
export function getTimeoutRedisCommandOptions(timeoutMs: number): CommandOptions {
6+
return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
7+
}

0 commit comments

Comments
 (0)