Skip to content

Commit ffd86b8

Browse files
authored
Store Next.js ISG cache on Redis (#4228)
1 parent 51d2301 commit ffd86b8

File tree

7 files changed

+211
-0
lines changed

7 files changed

+211
-0
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ jobs:
324324
CMS_ADMIN_HOST=${{ fromJSON('["pastaporto-", ""]')[github.ref == 'refs/heads/main'] }}admin.pycon.it
325325
CMS_HOSTNAME=${{ steps.vars.outputs.cms_hostname }}
326326
CONFERENCE_CODE=${{ steps.vars.outputs.conference_code }}
327+
GIT_HASH=${{ steps.git.outputs.githash }}
327328
328329
deploy-fe:
329330
runs-on: ubuntu-latest

frontend/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ARG API_URL_SERVER
1616
ARG CMS_HOSTNAME
1717
ARG CONFERENCE_CODE
1818
ARG CMS_ADMIN_HOST
19+
ARG GIT_HASH
1920

2021
WORKDIR /app
2122

frontend/cache-handler.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { CacheHandler } from "@neshca/cache-handler";
2+
import createLruHandler from "@neshca/cache-handler/local-lru";
3+
import createRedisHandler from "@neshca/cache-handler/redis-strings";
4+
import { createClient } from "redis";
5+
6+
CacheHandler.onCreation(async () => {
7+
let client;
8+
9+
try {
10+
// Create a Redis client.
11+
client = createClient({
12+
url: process.env.REDIS_URL ?? "redis://localhost:6379",
13+
});
14+
15+
// Redis won't work without error handling. https://github.com/redis/node-redis?tab=readme-ov-file#events
16+
client.on("error", (error) => {
17+
if (typeof process.env.NEXT_PRIVATE_DEBUG_CACHE !== "undefined") {
18+
// Use logging with caution in production. Redis will flood your logs. Hide it behind a flag.
19+
console.error("Redis client error:", error);
20+
}
21+
});
22+
} catch (error) {
23+
console.warn("Failed to create Redis client:", error);
24+
}
25+
26+
if (client) {
27+
try {
28+
console.info("Connecting Redis client...");
29+
30+
// Wait for the client to connect.
31+
// Caveat: This will block the server from starting until the client is connected.
32+
// And there is no timeout. Make your own timeout if needed.
33+
await client.connect();
34+
console.info("Redis client connected.");
35+
} catch (error) {
36+
console.warn("Failed to connect Redis client:", error);
37+
38+
console.warn("Disconnecting the Redis client...");
39+
// Try to disconnect the client to stop it from reconnecting.
40+
client
41+
.disconnect()
42+
.then(() => {
43+
console.info("Redis client disconnected.");
44+
})
45+
.catch(() => {
46+
console.warn(
47+
"Failed to quit the Redis client after failing to connect.",
48+
);
49+
});
50+
}
51+
}
52+
53+
/** @type {import("@neshca/cache-handler").Handler | null} */
54+
let handler;
55+
56+
if (client?.isReady) {
57+
// Create the `redis-stack` Handler if the client is available and connected.
58+
handler = await createRedisHandler({
59+
client,
60+
keyPrefix: `${process.env.GIT_HASH}:`,
61+
timeoutMs: 1000,
62+
keyExpirationStrategy: "EXAT",
63+
sharedTagsKey: "__sharedTags__",
64+
revalidateTagQuerySize: 100,
65+
});
66+
} else {
67+
// Fallback to LRU handler if Redis client is not available.
68+
// The application will still work, but the cache will be in memory only and not shared.
69+
handler = createLruHandler();
70+
console.warn(
71+
"Falling back to LRU handler because Redis client is not available.",
72+
);
73+
}
74+
75+
return {
76+
handlers: [handler],
77+
};
78+
});
79+
80+
export default CacheHandler;

frontend/next.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ module.exports = withSentryConfig({
2222
localeDetection: false,
2323
},
2424
trailingSlash: false,
25+
cacheHandler:
26+
process.env.VERCEL_ENV === "preview"
27+
? undefined
28+
: require.resolve("./cache-handler.mjs"),
29+
generateBuildId:
30+
process.env.VERCEL_ENV === "preview"
31+
? undefined
32+
: async () => {
33+
return process.env.GIT_HASH;
34+
},
35+
cacheMaxMemorySize: 0,
2536
async headers() {
2637
return [
2738
{

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@graphql-codegen/typescript": "^2.7.2",
2424
"@graphql-codegen/typescript-operations": "^2.5.2",
2525
"@graphql-codegen/typescript-react-apollo": "^4.3.2",
26+
"@neshca/cache-handler": "^1.9.0",
2627
"@python-italia/pycon-styleguide": "0.1.197",
2728
"@sentry/nextjs": "^8.24.0",
2829
"@vercel/analytics": "^1.1.1",
@@ -56,6 +57,7 @@
5657
"react-use-form-state": "^0.13.2",
5758
"react-use-sync-scroll": "^0.1.0",
5859
"react-wrap-balancer": "^1.1.1",
60+
"redis": "^4.7.0",
5961
"styled-system": "^5.1.5",
6062
"svg-to-pdfkit": "^0.1.8",
6163
"xstate": "^4.38.3",

frontend/pnpm-lock.yaml

Lines changed: 108 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/applications/pycon_frontend/task.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ resource "aws_ecs_task_definition" "pycon_frontend" {
3939
{
4040
name = "API_URL_SERVER",
4141
value = "http://${var.server_ip}"
42+
},
43+
{
44+
name = "REDIS_URL",
45+
value = "redis://${var.server_ip}/3"
46+
},
47+
{
48+
name = "GIT_HASH",
49+
value = data.external.githash.result.githash
4250
}
4351
]
4452

0 commit comments

Comments
 (0)