Skip to content

Commit 09f8508

Browse files
authored
feat(prerenderer): improving under high load (#599)
* feat(prerenderer): add prom + 503 overload protection * fix(prerenderer): configure trust proxy to fix http -> https redirects * fix(prerenderer): improving under high load
1 parent 9bb574a commit 09f8508

File tree

4 files changed

+165
-36
lines changed

4 files changed

+165
-36
lines changed

prerender-server/npm-shrinkwrap.json

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

prerender-server/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"dependencies": {
77
"body-parser": "^1.19.0",
88
"cookie-parser": "^1.4.5",
9-
"superagent": "^6.1.0",
10-
"prom-client": "^14.0.0"
9+
"express-queue": "^0.0.13",
10+
"prom-client": "^14.0.0",
11+
"superagent": "^6.1.0"
1112
},
1213
"scripts": {
1314
"start": "./start.sh",

prerender-server/src/cluster.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ if (cluster.isMaster) {
55
console.log(`Master ${process.pid} is running`);
66

77
let activeRenders = 0
8-
const MAX_TOTAL_RENDERS = parseInt(process.env.MAX_TOTAL_RENDERS || '1')
8+
const MAX_TOTAL_RENDERS = parseInt(process.env.MAX_TOTAL_RENDERS || numCPUs.toString())
99

1010
for (let i = 0; i < numCPUs; i++) {
1111
cluster.fork()

prerender-server/src/server.js

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import promClient from 'prom-client'
88
import l10n from '../client/l10n'
99
import render from '../client/run-server'
1010

11+
if (!process.env.API_URL) {
12+
throw new Error('API_URL environment variable is required but not defined.');
13+
}
14+
1115
const themes = ['light', 'dark']
1216
, langs = Object.keys(l10n)
1317
, baseHref = process.env.BASE_HREF || '/'
@@ -56,17 +60,30 @@ if (app.settings.env == 'development')
5660
app.use(require('cookie-parser')())
5761
app.use(require('body-parser').urlencoded({ extended: false }))
5862

59-
app.use((req, res, next) => {
60-
// TODO: optimize /block-height/nnn (no need to render the whole app just to get the redirect)
63+
const queue = process.env.MAX_PENDING_RENDERS && require('express-queue')({
64+
activeLimit: 1, // handled by the master process, see below
65+
queuedLimit: parseInt(process.env.MAX_PENDING_RENDERS, 10)
66+
});
6167

68+
app.use((req, res, next) => {
69+
// Middleware to check theme and lang cookies
6270
let theme = req.query.theme || req.cookies.theme || 'dark'
6371
if (!themes.includes(theme)) theme = 'light'
6472
if (req.query.theme && req.cookies.theme !== theme) res.cookie('theme', theme)
6573

6674
let lang = req.query.lang || req.cookies.lang || 'en'
6775
if (!langs.includes(lang)) lang = 'en'
6876
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
77+
req.renderOpts = { theme, lang }
78+
next()
79+
})
80+
81+
if (queue) app.use(queue)
82+
83+
app.use((req, res, next) => {
84+
// TODO: optimize /block-height/nnn (no need to render the whole app just to get the redirect)
6985

86+
// IPC-based queuing for cluster mode
7087
if (typeof process.send === 'function') {
7188
const requestId = ++requestCounter
7289
process.send({ type: 'startRender', requestId })
@@ -77,8 +94,9 @@ app.use((req, res, next) => {
7794
clearTimeout(timeout)
7895
process.removeListener('message', handler)
7996
if (msg.type === 'renderAllowed') {
80-
doRender()
97+
doRender(req, res, next)
8198
} else if (msg.type === 'renderDenied') {
99+
// received when the master's render queue is full
82100
res.status(503).send('Server overloaded')
83101
}
84102
}
@@ -93,40 +111,45 @@ app.use((req, res, next) => {
93111
}
94112
}, 5000) // 5 second timeout
95113
} else {
96-
doRender()
114+
// standalone mode
115+
doRender(req, res, next)
97116
}
117+
})
98118

99-
function doRender() {
100-
activeRenders.inc()
101-
const end = renderDuration.startTimer()
102-
let metricsUpdated = false
103-
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang, isHead: req.method === 'HEAD' }, (err, resp) => {
104-
if (!metricsUpdated) {
105-
metricsUpdated = true
106-
if (typeof process.send === 'function') process.send({ type: 'endRender' })
107-
activeRenders.dec()
108-
end()
109-
totalRenders.inc()
110-
}
111-
if (err) return next(err)
112-
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect)
113-
if (resp.errorCode) {
114-
console.error(`Failed with code ${resp.errorCode}:`, resp)
115-
return res.sendStatus(resp.errorCode)
116-
}
119+
function doRender(req, res, next) {
120+
activeRenders.inc()
121+
const end = renderDuration.startTimer()
122+
let metricsUpdated = false
123+
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { ...req.renderOpts, isHead: req.method === 'HEAD' }, (err, resp) => {
124+
if (!metricsUpdated) {
125+
metricsUpdated = true
126+
// inform the master process that we're done rendering and can accept new requests
127+
if (typeof process.send === 'function') process.send({ type: 'endRender' })
128+
// and tell express-queue that we're ready for the next one
129+
if (queue) queue.next()
130+
activeRenders.dec()
131+
end()
132+
totalRenders.inc()
133+
}
117134

118-
res.status(resp.status || 200)
119-
res.render(indexView, {
120-
prerender_title: resp.title
121-
, prerender_html: resp.html
122-
, canon_url: canonBase ? canonBase + req.url : null
123-
, noscript: true
124-
, theme
125-
, t: l10n[lang]
126-
})
135+
if (err) return next(err)
136+
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect)
137+
if (resp.errorCode) {
138+
console.error(`Failed with code ${resp.errorCode}:`, resp)
139+
return res.sendStatus(resp.errorCode)
140+
}
141+
142+
res.status(resp.status || 200)
143+
res.render(indexView, {
144+
prerender_title: resp.title
145+
, prerender_html: resp.html
146+
, canon_url: canonBase ? canonBase + req.url : null
147+
, noscript: true
148+
, ...req.renderOpts
149+
, t: l10n[req.renderOpts.lang]
127150
})
128-
}
129-
})
151+
})
152+
}
130153

131154
// Cleanup socket file from previous executions
132155
if (process.env.SOCKET_PATH) {

0 commit comments

Comments
 (0)