Skip to content

Commit fe40f72

Browse files
committed
fix(prerenderer): improving under high load
1 parent 414a17e commit fe40f72

File tree

4 files changed

+163
-37
lines changed

4 files changed

+163
-37
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)
99

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

prerender-server/src/server.js

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const themes = ['light', 'dark']
1212
, langs = Object.keys(l10n)
1313
, baseHref = process.env.BASE_HREF || '/'
1414
, canonBase = process.env.CANONICAL_URL ? process.env.CANONICAL_URL.replace(/\/$/, '') : null
15-
, apiUrl = process.env.API_URL.replace(/\/$/, '')
15+
, apiUrl = process.env.API_URL ? process.env.API_URL.replace(/\/$/, '') : ''
1616

1717
const rpath = p => path.join(__dirname, p)
1818

@@ -56,17 +56,30 @@ if (app.settings.env == 'development')
5656
app.use(require('cookie-parser')())
5757
app.use(require('body-parser').urlencoded({ extended: false }))
5858

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

64+
app.use((req, res, next) => {
65+
// Middleware to check theme and lang cookies
6266
let theme = req.query.theme || req.cookies.theme || 'dark'
6367
if (!themes.includes(theme)) theme = 'light'
6468
if (req.query.theme && req.cookies.theme !== theme) res.cookie('theme', theme)
6569

6670
let lang = req.query.lang || req.cookies.lang || 'en'
6771
if (!langs.includes(lang)) lang = 'en'
6872
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
73+
req.renderOpts = { theme, lang }
74+
next()
75+
})
76+
77+
if (queue) app.use(queue)
6978

79+
app.use((req, res, next) => {
80+
// TODO: optimize /block-height/nnn (no need to render the whole app just to get the redirect)
81+
82+
// IPC-based queuing for cluster mode
7083
if (typeof process.send === 'function') {
7184
const requestId = ++requestCounter
7285
process.send({ type: 'startRender', requestId })
@@ -77,8 +90,9 @@ app.use((req, res, next) => {
7790
clearTimeout(timeout)
7891
process.removeListener('message', handler)
7992
if (msg.type === 'renderAllowed') {
80-
doRender()
93+
doRender(req, res, next)
8194
} else if (msg.type === 'renderDenied') {
95+
// received when the master's render queue is full
8296
res.status(503).send('Server overloaded')
8397
}
8498
}
@@ -90,43 +104,49 @@ app.use((req, res, next) => {
90104
process.removeListener('message', handler)
91105
console.error('IPC timeout for request', requestId)
92106
res.status(500).send('Internal server error')
107+
if (typeof process.send === 'function') process.send({ type: 'endRender' })
93108
}
94109
}, 5000) // 5 second timeout
95110
} else {
96-
doRender()
111+
// standalone mode
112+
doRender(req, res, next)
97113
}
114+
})
98115

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-
}
116+
function doRender(req, res, next) {
117+
activeRenders.inc()
118+
const end = renderDuration.startTimer()
119+
let metricsUpdated = false
120+
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { ...req.renderOpts, isHead: req.method === 'HEAD' }, (err, resp) => {
121+
if (!metricsUpdated) {
122+
metricsUpdated = true
123+
// inform the master process that we're done rendering and can accept new requests
124+
if (typeof process.send === 'function') process.send({ type: 'endRender' })
125+
// and tell express-queue that we're ready for the next one
126+
if (queue) queue.next()
127+
activeRenders.dec()
128+
end()
129+
totalRenders.inc()
130+
}
117131

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-
})
132+
if (err) return next(err)
133+
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect)
134+
if (resp.errorCode) {
135+
console.error(`Failed with code ${resp.errorCode}:`, resp)
136+
return res.sendStatus(resp.errorCode)
137+
}
138+
139+
res.status(resp.status || 200)
140+
res.render(indexView, {
141+
prerender_title: resp.title
142+
, prerender_html: resp.html
143+
, canon_url: canonBase ? canonBase + req.url : null
144+
, noscript: true
145+
, ...req.renderOpts
146+
, t: l10n[req.renderOpts.lang]
127147
})
128-
}
129-
})
148+
})
149+
}
130150

131151
// Cleanup socket file from previous executions
132152
if (process.env.SOCKET_PATH) {

0 commit comments

Comments
 (0)