Skip to content

Commit

Permalink
Replace the old redis cache implementation (#858)
Browse files Browse the repository at this point in the history
I created a new version of the Redis cache engine.

Currently it supports the put, match and delete methods. The connection and methods will not block the load of the pages.

It was made to also support reconnection on failure; and even during a failure the website will keep running until the connection gets healthy again.

When an error occurs, the connection will be halted on purpose. Just like a open circuit breaker. The idea is to not fall into a reconnection loop. Once the website tries to cache again, the engine will try to reconnect.

Co-authored-by: Marcos Candeia <[email protected]>
  • Loading branch information
azisaka and mcandeia authored Oct 10, 2024
1 parent 2590f1d commit ea0d44f
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 1 deletion.
10 changes: 10 additions & 0 deletions runtime/caches/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { isFileSystemAvailable } from "./fileSystem.ts";

import { caches as headersCache } from "./headerscache.ts";

import {
caches as redisCache,
isAvailable as isRedisCacheAvailable,
} from "./redis.ts";

import { createTieredCache } from "./tiered.ts";

import { caches as lruCache } from "./lrucache.ts";
Expand All @@ -24,6 +29,7 @@ export interface CacheStorageOption {

export type CacheEngine =
| "CACHE_API"
| "REDIS"
| "FILE_SYSTEM";

export const cacheImplByEngine: Record<CacheEngine, CacheStorageOption> = {
Expand All @@ -35,6 +41,10 @@ export const cacheImplByEngine: Record<CacheEngine, CacheStorageOption> = {
implementation: headersCache(lruCache(fileSystem)),
isAvailable: isFileSystemAvailable,
},
REDIS: {
implementation: redisCache,
isAvailable: isRedisCacheAvailable,
},
};

for (const [engine, cache] of Object.entries(cacheImplByEngine)) {
Expand Down
139 changes: 139 additions & 0 deletions runtime/caches/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
assertCanBeCached,
assertNoOptions,
baseCache,
NOT_IMPLEMENTED,
withCacheNamespace,
} from "./utils.ts";
import {
createClient,
type RedisClientType,
type RedisFunctions,
type RedisModules,
type RedisScripts,
} from "npm:@redis/client@^1.6.0";

const CONNECTION_TIMEOUT = 500;
const RECONNECTION_TIMEOUT = 5000;

type RedisConnection = RedisClientType<
RedisModules,
RedisFunctions,
RedisScripts
>;

let redis: null | RedisConnection = null;

export const isAvailable = Deno.env.has("LOADER_CACHE_REDIS_URL");

function connect(): void {
if (!isAvailable) {
return;
}

redis ??= createClient({
url: Deno.env.get("LOADER_CACHE_REDIS_URL"),
});

redis.on("error", () => {
if (redis?.isOpen) {
redis?.disconnect();
}

wait(RECONNECTION_TIMEOUT).then(() => redis?.connect());
});

redis.connect();
}

async function serialize(response: Response): Promise<string> {
const body = await response.text();

return JSON.stringify({
body: body,
headers: response.headers,
status: response.status,
});
}

function deserialize(raw: string): Response {
const { body, headers, status } = JSON.parse(raw);
return new Response(body, { headers, status });
}

function wait(ms: number) {
return new Promise((run) => setTimeout(run, ms));
}

export const caches: CacheStorage = {
open: async (namespace: string): Promise<Cache> => {
await Promise.race([connect(), wait(CONNECTION_TIMEOUT)]);

return Promise.resolve({
...baseCache,
delete: async (
request: RequestInfo | URL,
_?: CacheQueryOptions,
): Promise<boolean> => {
const generateKey = withCacheNamespace(namespace);

return generateKey(request)
.then((cacheKey: string) => {
redis?.del(cacheKey);

return true;
})
.catch(() => false);
},
match: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<Response | undefined> => {
assertNoOptions(options);

const generateKey = withCacheNamespace(namespace);

return generateKey(request)
.then((cacheKey: string) => redis?.get(cacheKey) ?? null)
.then((result: string | null) => {
if (!result) {
return undefined;
}

return deserialize(result);
})
.catch(() => undefined);
},
put: async (
request: RequestInfo | URL,
response: Response,
): Promise<void> => {
const req = new Request(request);
assertCanBeCached(req, response);

if (!response.body) {
return;
}

const generateKey = withCacheNamespace(namespace);
const cacheKey = await generateKey(request);

serialize(response)
.then((data) => {
const expirationTimestamp = Date.parse(
response.headers.get("expires") ?? "",
);

const ttl = expirationTimestamp - Date.now();

return redis?.set(cacheKey, data, { PX: ttl });
})
.catch(() => {});
},
});
},
delete: NOT_IMPLEMENTED,
has: NOT_IMPLEMENTED,
keys: NOT_IMPLEMENTED,
match: NOT_IMPLEMENTED,
};
2 changes: 1 addition & 1 deletion runtime/caches/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const sha1 = async (text: string) => {
return hex;
};

const NOT_IMPLEMENTED = () => {
export const NOT_IMPLEMENTED = () => {
throw new Error("Not Implemented");
};

Expand Down

0 comments on commit ea0d44f

Please sign in to comment.