Skip to content

Commit

Permalink
Refactor Redis Handlers timeout handling (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
better-salmon authored Jan 13, 2024
1 parent a0b23ae commit c62c986
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 58 deletions.
10 changes: 10 additions & 0 deletions .changeset/hip-taxis-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@neshca/cache-handler': patch
---

Refactored Redis Handlers timeout handling

#### Changes

- Refactored Redis Handlers to use `AbortSignal` instead of promisifying `setTimeout`.
- Set default Redis Handlers `timeoutMs` option to 5000 ms.
8 changes: 2 additions & 6 deletions apps/cache-testing/cache-handler-redis-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@ if (!process.env.REDIS_URL) {
console.warn('Make sure that REDIS_URL is added to the .env.local file and loaded properly.');
}

const CONNECT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes

const client = createClient({
url: process.env.REDIS_URL,
name: `cache-handler:${process.env.PORT ?? process.pid}`,
socket: {
connectTimeout: CONNECT_TIMEOUT_MS,
},
});

client.on('error', (error) => {
Expand All @@ -30,9 +25,10 @@ IncrementalCache.onCreation(async () => {
const redisCache = await createRedisCache({
client,
keyPrefix: 'JSON:',
timeoutMs: 1000,
});

const redisStringsCache = createRedisStringsCache({ client, keyPrefix: 'strings:' });
const redisStringsCache = createRedisStringsCache({ client, keyPrefix: 'strings:', timeoutMs: 1000 });

const localCache = createLruCache();

Expand Down
8 changes: 3 additions & 5 deletions apps/cache-testing/cache-handler-redis-strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@ if (!process.env.REDIS_URL) {
}

const PREFIX = 'string:';
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000;

const client = createClient({
url: process.env.REDIS_URL,
name: `cache-handler:${PREFIX}${process.env.PORT ?? process.pid}`,
socket: {
connectTimeout: CONNECT_TIMEOUT_MS,
},
});

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

IncrementalCache.onCreation(async () => {
console.log('Connecting Redis client...');
await client.connect();
console.log('Redis client connected.');

const redisCache = await createRedisCache({
const redisCache = createRedisCache({
client,
keyPrefix: PREFIX,
});
Expand Down
2 changes: 1 addition & 1 deletion docs/cache-handler-docs/theme.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default {
key: '0.6.0-release',
text: (
<a href={`${process.env.NEXT_PUBLIC_BASE_URL}`}>
🎉 Version 0.6.4 is out, offering stale-while-revalidate strategy emulation!
🎉 Version 0.6.5 is out, offering stale-while-revalidate strategy emulation and codebase improvements!
</a>
),
},
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/cache-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ npm run e2e:ui -w @neshca/cache-testing
**Important**: Clear the Redis DB before executing tests:
```bash
docker exec -it redis-stack redis-cli
docker exec -it cache-handler-redis redis-cli
127.0.0.1:6379> flushall
OK
```
Expand Down
2 changes: 1 addition & 1 deletion packages/cache-handler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

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

Expand Down
58 changes: 30 additions & 28 deletions packages/cache-handler/src/handlers/redis-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import type { RedisClientType } from 'redis';
import type { RevalidatedTags, CacheHandlerValue, Cache } from '../cache-handler';
import type { RedisJSON, UseTtlOptions } from '../common-types';
import { promiseWithTimeout } from '../helpers/promise-with-timeout';
import { calculateEvictionDelay } from '../helpers/calculate-eviction-delay';
import { getTimeoutRedisCommandOptions } from '../helpers/get-timeout-redis-command-options';

/**
* The configuration options for the Redis Handler
Expand All @@ -22,7 +22,7 @@ export type RedisCacheHandlerOptions<T> = UseTtlOptions & {
*/
revalidatedTagsKey?: string;
/**
* Timeout in milliseconds for Redis operations.
* Timeout in milliseconds for Redis operations. Defaults to 5000.
*/
timeoutMs?: number;
};
Expand Down Expand Up @@ -60,7 +60,7 @@ export default async function createCache<T extends RedisClientType>({
keyPrefix = '',
revalidatedTagsKey = '__sharedRevalidatedTags__',
useTtl = false,
timeoutMs,
timeoutMs = 5000,
}: RedisCacheHandlerOptions<T>): Promise<Cache> {
function assertClientIsReady(): void {
if (!client.isReady) {
Expand All @@ -70,7 +70,8 @@ export default async function createCache<T extends RedisClientType>({

assertClientIsReady();

const setInitialRevalidatedTags = client.json.set(
await client.json.set(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + revalidatedTagsKey,
'.',
{},
Expand All @@ -79,17 +80,15 @@ export default async function createCache<T extends RedisClientType>({
},
);

await promiseWithTimeout(setInitialRevalidatedTags, timeoutMs);

return {
name: 'redis-stack',
async get(key) {
assertClientIsReady();

const getCacheValue = client.json.get(keyPrefix + key);

const cacheValue = ((await promiseWithTimeout(getCacheValue, timeoutMs)) ??
null) as CacheHandlerValue | null;
const cacheValue = (await client.json.get(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + key,
)) as CacheHandlerValue | null;

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

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

await promiseWithTimeout(setCacheValue, timeoutMs);
const setCacheValue = client.json.set(
options,
keyPrefix + key,
'.',
preparedCacheValue as unknown as RedisJSON,
);

if (typeof maxAgeSeconds !== 'number') {
return;
}
const commands: Promise<unknown>[] = [setCacheValue];

const evictionDelay = calculateEvictionDelay(maxAgeSeconds, useTtl);

if (!evictionDelay) {
return;
if (evictionDelay) {
commands.push(client.expire(options, keyPrefix + key, evictionDelay));
}

const setExpire = client.expire(keyPrefix + key, evictionDelay);

await promiseWithTimeout(setExpire, timeoutMs);
await Promise.all(commands);
},
async getRevalidatedTags() {
assertClientIsReady();

const getRevalidatedTags = client.json.get(keyPrefix + revalidatedTagsKey);

const sharedRevalidatedTags = ((await promiseWithTimeout(getRevalidatedTags, timeoutMs)) ?? undefined) as
| RevalidatedTags
| undefined;
const sharedRevalidatedTags = (await client.json.get(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + revalidatedTagsKey,
)) as RevalidatedTags | undefined;

return sharedRevalidatedTags;
},
async revalidateTag(tag, revalidatedAt) {
assertClientIsReady();

const setRevalidatedTags = client.json.set(keyPrefix + revalidatedTagsKey, `.${tag}`, revalidatedAt);

await promiseWithTimeout(setRevalidatedTags, timeoutMs);
await client.json.set(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + revalidatedTagsKey,
`.${tag}`,
revalidatedAt,
);
},
};
}
30 changes: 14 additions & 16 deletions packages/cache-handler/src/handlers/redis-strings.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable import/no-default-export -- use default here */
import { reviveFromBase64Representation, replaceJsonWithBase64 } from '@neshca/json-replacer-reviver';
import { replaceJsonWithBase64, reviveFromBase64Representation } from '@neshca/json-replacer-reviver';
import type { RedisClientType } from 'redis';
import { promiseWithTimeout } from '../helpers/promise-with-timeout';
import type { RevalidatedTags, CacheHandlerValue, Cache } from '../cache-handler';
import type { Cache, CacheHandlerValue, RevalidatedTags } from '../cache-handler';
import { calculateEvictionDelay } from '../helpers/calculate-eviction-delay';
import { getTimeoutRedisCommandOptions } from '../helpers/get-timeout-redis-command-options';
import type { RedisCacheHandlerOptions } from './redis-stack';

/**
Expand Down Expand Up @@ -39,11 +39,11 @@ export default function createCache<T extends RedisClientType>({
keyPrefix = '',
revalidatedTagsKey = '__sharedRevalidatedTags__',
useTtl = false,
timeoutMs,
timeoutMs = 5000,
}: RedisCacheHandlerOptions<T>): Cache {
function assertClientIsReady(): void {
if (!client.isReady) {
throw new Error('Redis client is not ready');
throw new Error('Redis client is not ready yet or connection is lost. Keep trying...');
}
}

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

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

const result = await promiseWithTimeout(getOperation, timeoutMs);
const result = await getOperation;

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

// use replaceJsonWithBase64 to store binary data in Base64 and save space
const setOperation = client.set(
await client.set(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + key,
JSON.stringify(value, replaceJsonWithBase64),
evictionDelay ? { EX: evictionDelay } : undefined,
);

await promiseWithTimeout(setOperation, timeoutMs);
},
async getRevalidatedTags() {
assertClientIsReady();

const getOperation = client.hGetAll(keyPrefix + revalidatedTagsKey);

const sharedRevalidatedTags = await promiseWithTimeout(getOperation, timeoutMs);
const sharedRevalidatedTags = await client.hGetAll(
getTimeoutRedisCommandOptions(timeoutMs),
keyPrefix + revalidatedTagsKey,
);

const entries = Object.entries(sharedRevalidatedTags);

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

const setOperation = client.hSet(keyPrefix + revalidatedTagsKey, {
await client.hSet(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + revalidatedTagsKey, {
[tag]: revalidatedAt,
});

await promiseWithTimeout(setOperation, timeoutMs);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { commandOptions } from 'redis';

type CommandOptions = ReturnType<typeof commandOptions>;

export function getTimeoutRedisCommandOptions(timeoutMs: number): CommandOptions {
return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
}

0 comments on commit c62c986

Please sign in to comment.