From 623efc28c7fc5a7a6cfe7ce8679114e149eac30a Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Sat, 10 May 2025 10:48:03 +0200 Subject: [PATCH] chore: revert to old caching handler to increase stability --- apps/web/cache-handler.js | 97 -------------------------------------- apps/web/cache-handler.mjs | 81 +++++++++++++++++++++++++++++++ apps/web/next.config.mjs | 2 +- 3 files changed, 82 insertions(+), 98 deletions(-) delete mode 100644 apps/web/cache-handler.js create mode 100644 apps/web/cache-handler.mjs diff --git a/apps/web/cache-handler.js b/apps/web/cache-handler.js deleted file mode 100644 index 9a8ecdd59c..0000000000 --- a/apps/web/cache-handler.js +++ /dev/null @@ -1,97 +0,0 @@ -// This cache handler follows the @fortedigital/nextjs-cache-handler example -// Read more at: https://github.com/fortedigital/nextjs-cache-handler - -// @neshca/cache-handler dependencies -const { CacheHandler } = require("@neshca/cache-handler"); -const createLruHandler = require("@neshca/cache-handler/local-lru").default; - -// Next/Redis dependencies -const { createClient } = require("redis"); -const { PHASE_PRODUCTION_BUILD } = require("next/constants"); - -// @fortedigital/nextjs-cache-handler dependencies -const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default; -const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler"); - -// Usual onCreation from @neshca/cache-handler -CacheHandler.onCreation(() => { - // Important - It's recommended to use global scope to ensure only one Redis connection is made - // This ensures only one instance get created - if (global.cacheHandlerConfig) { - return global.cacheHandlerConfig; - } - - // Important - It's recommended to use global scope to ensure only one Redis connection is made - // This ensures new instances are not created in a race condition - if (global.cacheHandlerConfigPromise) { - return global.cacheHandlerConfigPromise; - } - - // If REDIS_URL is not set, we will use LRU cache only - if (!process.env.REDIS_URL) { - const lruCache = createLruHandler(); - return { handlers: [lruCache] }; - } - - // Main promise initializing the handler - global.cacheHandlerConfigPromise = (async () => { - /** @type {import("redis").RedisClientType | null} */ - let redisClient = null; - // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable - if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) { - const settings = { - url: process.env.REDIS_URL, // Make sure you configure this variable - pingInterval: 10000, - }; - - try { - redisClient = createClient(settings); - redisClient.on("error", (e) => { - console.error("Redis error", e); - global.cacheHandlerConfig = null; - global.cacheHandlerConfigPromise = null; - }); - } catch (error) { - console.error("Failed to create Redis client:", error); - } - } - - if (redisClient) { - try { - console.info("Connecting Redis client..."); - await redisClient.connect(); - console.info("Redis client connected."); - } catch (error) { - console.error("Failed to connect Redis client:", error); - await redisClient - .disconnect() - .catch(() => console.error("Failed to quit the Redis client after failing to connect.")); - } - } - const lruCache = createLruHandler(); - - if (!redisClient?.isReady) { - console.error("Failed to initialize caching layer."); - global.cacheHandlerConfigPromise = null; - global.cacheHandlerConfig = { handlers: [lruCache] }; - return global.cacheHandlerConfig; - } - - const redisCacheHandler = createRedisHandler({ - client: redisClient, - keyPrefix: "nextjs:", - }); - - global.cacheHandlerConfigPromise = null; - - global.cacheHandlerConfig = { - handlers: [redisCacheHandler], - }; - - return global.cacheHandlerConfig; - })(); - - return global.cacheHandlerConfigPromise; -}); - -module.exports = new Next15CacheHandler(); diff --git a/apps/web/cache-handler.mjs b/apps/web/cache-handler.mjs new file mode 100644 index 0000000000..8f95c8673b --- /dev/null +++ b/apps/web/cache-handler.mjs @@ -0,0 +1,81 @@ +import { Next15CacheHandler } from "@fortedigital/nextjs-cache-handler/next-15-cache-handler"; +import createRedisHandler from "@fortedigital/nextjs-cache-handler/redis-strings"; +import { CacheHandler } from "@neshca/cache-handler"; +import createLruHandler from "@neshca/cache-handler/local-lru"; +import { createClient } from "redis"; + +// Function to create a timeout promise +const createTimeoutPromise = (ms, rejectReason) => { + return new Promise((_, reject) => setTimeout(() => reject(new Error(rejectReason)), ms)); +}; + +CacheHandler.onCreation(async () => { + let client; + + if (process.env.REDIS_URL) { + try { + // Create a Redis client. + client = createClient({ + url: process.env.REDIS_URL, + }); + + // Redis won't work without error handling. + client.on("error", () => {}); + } catch (error) { + console.warn("Failed to create Redis client:", error); + } + + if (client) { + try { + // Wait for the client to connect with a timeout of 5000ms. + const connectPromise = client.connect(); + const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout + await Promise.race([connectPromise, timeoutPromise]); + } catch (error) { + console.warn("Failed to connect Redis client:", error); + + console.warn("Disconnecting the Redis client..."); + // Try to disconnect the client to stop it from reconnecting. + client + .disconnect() + .then(() => { + console.info("Redis client disconnected."); + }) + .catch(() => { + console.warn("Failed to quit the Redis client after failing to connect."); + }); + } + } + } + + /** @type {import("@neshca/cache-handler").Handler | null} */ + let handler; + + if (client?.isReady) { + const redisHandlerOptions = { + client, + keyPrefix: "fb:", + timeoutMs: 1000, + }; + + // Create the `redis-stack` Handler if the client is available and connected. + handler = await createRedisHandler(redisHandlerOptions); + } else { + // Fallback to LRU handler if Redis client is not available. + // The application will still work, but the cache will be in memory only and not shared. + handler = createLruHandler(); + console.log("Using LRU handler for caching."); + } + + return { + handlers: [handler], + ttl: { + // We set the stale and the expire age to the same value, because the stale age is determined by the unstable_cache revalidation. + defaultStaleAge: (process.env.REDIS_URL && Number(process.env.REDIS_DEFAULT_TTL)) || 86400, + estimateExpireAge: (staleAge) => staleAge, + }, + }; +}); + +const cacheHandler = new Next15CacheHandler(); +export default cacheHandler; diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ddc3bfc9df..9cc4b1384e 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -16,7 +16,7 @@ const getHostname = (url) => { const nextConfig = { assetPrefix: process.env.ASSET_PREFIX_URL || undefined, - cacheHandler: require.resolve("./cache-handler.js"), + cacheHandler: require.resolve("./cache-handler.mjs"), cacheMaxMemorySize: 0, // disable default in-memory caching output: "standalone", poweredByHeader: false,