Skip to content

Commit 02deb64

Browse files
committed
feat: remove general timeoutMs and replace it with getTimeoutMs (for faster non-blocking rendering)
1 parent 275176d commit 02deb64

File tree

3 files changed

+42
-89
lines changed

3 files changed

+42
-89
lines changed

README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -123,22 +123,22 @@ A working example of above can be found in the `test/integration/next-app-custom
123123

124124
## Available Options
125125

126-
| Option | Description | Default Value |
127-
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128-
| redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` |
129-
| database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` |
130-
| keyPrefix | Prefix added to all Redis keys | `process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'` |
131-
| sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` |
132-
| timeoutMs | Timeout in milliseconds for Redis operations | `Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 5_000 : 5_000` |
133-
| revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` |
134-
| avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) |
135-
| redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` |
136-
| inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` |
137-
| defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) |
138-
| estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`<br> Other: `staleAge * 1.2` |
139-
| socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` |
140-
| clientOptions | Additional Redis client options (e.g., username, password) | `undefined` |
141-
| killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` |
126+
| Option | Description | Default Value |
127+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128+
| redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` |
129+
| database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` |
130+
| keyPrefix | Prefix added to all Redis keys | `process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'` |
131+
| sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` |
132+
| getTimeoutMs | Timeout in milliseconds for time critical Redis operations. If Redis get is not fulfilled within this time, returns null to avoid blocking site rendering. | `process.env.REDIS_COMMAND_TIMEOUT_MS ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) : 500` |
133+
| revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` |
134+
| avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) |
135+
| redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` |
136+
| inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` |
137+
| defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) |
138+
| estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`<br> Other: `staleAge * 1.2` |
139+
| socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` |
140+
| clientOptions | Additional Redis client options (e.g., username, password) | `undefined` |
141+
| killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` |
142142

143143
## TLS Configuration
144144

src/RedisStringsHandler.ts

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function redisErrorHandler<T extends Promise<unknown>>(
3030
}) as T;
3131
}
3232

33-
// This is a test to check if the event loop is lagging. Increase CPU
33+
// This is a test to check if the event loop is lagging. If it lags, increase CPU of container
3434
setInterval(() => {
3535
const start = performance.now();
3636
setImmediate(() => {
@@ -58,10 +58,12 @@ export type CreateRedisStringsHandlerOptions = {
5858
* @default process.env.VERCEL_URL || 'UNDEFINED_URL_'
5959
*/
6060
keyPrefix?: string;
61-
/** Timeout in milliseconds for Redis operations
62-
* @default 5000
61+
/** Timeout in milliseconds for time critical Redis operations (during cache get, which blocks site rendering).
62+
* If redis get is not fulfilled within this time, the cache handler will return null so site rendering will
63+
* not be blocked further and site can fallback to re-render/re-fetch the content.
64+
* @default 500
6365
*/
64-
timeoutMs?: number;
66+
getTimeoutMs?: number;
6567
/** Number of entries to query in one batch during full sync of shared tags hash map
6668
* @default 250
6769
*/
@@ -112,12 +114,6 @@ const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_';
112114
// This helps track when specific tags were last invalidated
113115
const REVALIDATED_TAGS_KEY = '__revalidated_tags__';
114116

115-
export function getTimeoutRedisCommandOptions(
116-
timeoutMs: number,
117-
): CommandOptions {
118-
return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
119-
}
120-
121117
let killContainerOnErrorCount: number = 0;
122118
export default class RedisStringsHandler {
123119
private client: Client;
@@ -126,13 +122,13 @@ export default class RedisStringsHandler {
126122
private inMemoryDeduplicationCache: SyncedMap<
127123
Promise<ReturnType<Client['get']>>
128124
>;
125+
private getTimeoutMs: number;
129126
private redisGet: Client['get'];
130127
private redisDeduplicationHandler: DeduplicatedRequestHandler<
131128
Client['get'],
132129
string | Buffer | null
133130
>;
134131
private deduplicatedRedisGet: (key: string) => Client['get'];
135-
private timeoutMs: number;
136132
private keyPrefix: string;
137133
private redisGetDeduplication: boolean;
138134
private inMemoryCachingTime: number;
@@ -149,9 +145,9 @@ export default class RedisStringsHandler {
149145
database = process.env.VERCEL_ENV === 'production' ? 0 : 1,
150146
keyPrefix = process.env.VERCEL_URL || 'UNDEFINED_URL_',
151147
sharedTagsKey = '__sharedTags__',
152-
timeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS
153-
? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 5_000)
154-
: 5_000,
148+
getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS
149+
? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500)
150+
: 500,
155151
revalidateTagQuerySize = 250,
156152
avgResyncIntervalMs = 60 * 60 * 1_000,
157153
redisGetDeduplication = true,
@@ -168,12 +164,12 @@ export default class RedisStringsHandler {
168164
}: CreateRedisStringsHandlerOptions) {
169165
try {
170166
this.keyPrefix = keyPrefix;
171-
this.timeoutMs = timeoutMs;
172167
this.redisGetDeduplication = redisGetDeduplication;
173168
this.inMemoryCachingTime = inMemoryCachingTime;
174169
this.defaultStaleAge = defaultStaleAge;
175170
this.estimateExpireAge = estimateExpireAge;
176171
this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
172+
this.getTimeoutMs = getTimeoutMs;
177173

178174
try {
179175
// Create Redis client with properly typed configuration
@@ -243,7 +239,6 @@ export default class RedisStringsHandler {
243239
keyPrefix,
244240
redisKey: sharedTagsKey,
245241
database,
246-
timeoutMs,
247242
querySize: revalidateTagQuerySize,
248243
filterKeys,
249244
resyncIntervalMs:
@@ -257,7 +252,6 @@ export default class RedisStringsHandler {
257252
keyPrefix,
258253
redisKey: REVALIDATED_TAGS_KEY,
259254
database,
260-
timeoutMs,
261255
querySize: revalidateTagQuerySize,
262256
filterKeys,
263257
resyncIntervalMs:
@@ -271,7 +265,6 @@ export default class RedisStringsHandler {
271265
keyPrefix,
272266
redisKey: 'inMemoryDeduplicationCache',
273267
database,
274-
timeoutMs,
275268
querySize: revalidateTagQuerySize,
276269
filterKeys,
277270
customizedSync: {
@@ -332,7 +325,7 @@ export default class RedisStringsHandler {
332325
'assertClientIsReady: Timeout waiting for Redis maps to be ready',
333326
),
334327
);
335-
}, this.timeoutMs * 5),
328+
}, 30_000),
336329
),
337330
]);
338331
this.clientReadyCalls = 0;
@@ -385,14 +378,14 @@ export default class RedisStringsHandler {
385378
const serializedCacheEntry = await redisErrorHandler(
386379
'RedisStringsHandler.get(), operation: get' +
387380
(this.redisGetDeduplication ? 'deduplicated' : '') +
388-
this.timeoutMs +
389-
'ms' +
390381
' ' +
382+
this.getTimeoutMs +
383+
'ms ' +
391384
this.keyPrefix +
392385
' ' +
393386
key,
394387
clientGet(
395-
getTimeoutRedisCommandOptions(this.timeoutMs),
388+
commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
396389
this.keyPrefix + key,
397390
),
398391
);
@@ -474,10 +467,11 @@ export default class RedisStringsHandler {
474467
// We don't await this cleanup since it can happen asynchronously in the background.
475468
// The cache entry is already considered invalid at this point.
476469
this.client
477-
.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey)
470+
.unlink(redisKey)
478471
.catch((err) => {
479-
// If the first unlink fails, only log the error
480-
// Never implement a retry here as the cache entry will be updated directly after this get request
472+
// Log error but don't retry deletion since the cache entry will likely be
473+
// updated immediately after via set(). A retry could dangerously execute
474+
// after the new value is set.
481475
console.error(
482476
'Error occurred while unlinking stale data. Error was:',
483477
err,
@@ -634,16 +628,12 @@ export default class RedisStringsHandler {
634628
: this.estimateExpireAge(this.defaultStaleAge);
635629

636630
// Setting the cache entry in redis
637-
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
638631
const setOperation: Promise<string | null> = redisErrorHandler(
639-
'RedisStringsHandler.set(), operation: set' +
640-
this.timeoutMs +
641-
'ms' +
642-
' ' +
632+
'RedisStringsHandler.set(), operation: set ' +
643633
this.keyPrefix +
644634
' ' +
645635
key,
646-
this.client.set(options, this.keyPrefix + key, serializedCacheEntry, {
636+
this.client.set(this.keyPrefix + key, serializedCacheEntry, {
647637
EX: expireAt,
648638
}),
649639
);
@@ -768,16 +758,12 @@ export default class RedisStringsHandler {
768758
// prepare deletion of all keys in redis that are related to this tag
769759
const redisKeys = Array.from(keysToDelete);
770760
const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
771-
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
772761
const deleteKeysOperation = redisErrorHandler(
773-
'RedisStringsHandler.revalidateTag(), operation: unlink' +
774-
this.timeoutMs +
775-
'ms' +
776-
' ' +
762+
'RedisStringsHandler.revalidateTag(), operation: unlink ' +
777763
this.keyPrefix +
778764
' ' +
779765
fullRedisKeys,
780-
this.client.unlink(options, fullRedisKeys),
766+
this.client.unlink(fullRedisKeys),
781767
);
782768

783769
// also delete entries from in-memory deduplication cache if they get revalidated

0 commit comments

Comments
 (0)