Skip to content

Commit f1a118c

Browse files
authored
feat(prerenderer): add prom + 503 overload protection (#595)
* feat(prerenderer): add prom + 503 overload protection * fix(prerenderer): configure trust proxy to fix http -> https redirects (#596)
1 parent 54a8453 commit f1a118c

File tree

3 files changed

+105
-18
lines changed

3 files changed

+105
-18
lines changed

prerender-server/package.json

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

prerender-server/src/cluster.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
const cluster = require('cluster')
2-
, numCPUs = require('os').cpus().length
2+
, numCPUs = require('os').cpus().length
33

44
if (cluster.isMaster) {
55
console.log(`Master ${process.pid} is running`);
66

7+
let activeRenders = 0
8+
const MAX_TOTAL_RENDERS = parseInt(process.env.MAX_TOTAL_RENDERS || '1')
9+
710
for (let i = 0; i < numCPUs; i++) {
811
cluster.fork()
912
}
1013

14+
cluster.on('message', (worker, message) => {
15+
if (message.type === 'startRender') {
16+
if (activeRenders < MAX_TOTAL_RENDERS) {
17+
activeRenders++
18+
worker.send({ type: 'renderAllowed', requestId: message.requestId })
19+
} else {
20+
worker.send({ type: 'renderDenied', requestId: message.requestId })
21+
}
22+
} else if (message.type === 'endRender') {
23+
activeRenders = Math.max(0, activeRenders - 1)
24+
}
25+
})
26+
1127
cluster.on('exit', (worker, code, signal) => {
1228
console.log(`worker ${worker.process.pid} died`, { worker, code, signal });
1329
});

prerender-server/src/server.js

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import pug from 'pug'
33
import path from 'path'
44
import express from 'express'
55
import request from 'superagent'
6+
import promClient from 'prom-client'
67

78
import l10n from '../client/l10n'
89
import render from '../client/run-server'
@@ -17,9 +18,38 @@ const rpath = p => path.join(__dirname, p)
1718

1819
const indexView = rpath('../../client/index.pug')
1920

21+
const register = new promClient.Registry()
22+
23+
const activeRenders = new promClient.Gauge({
24+
name: 'prerender_active_renders',
25+
help: 'Number of active renders'
26+
})
27+
28+
const totalRenders = new promClient.Counter({
29+
name: 'prerender_total_renders',
30+
help: 'Total number of renders completed'
31+
})
32+
33+
const renderDuration = new promClient.Histogram({
34+
name: 'prerender_render_duration_seconds',
35+
help: 'Duration of renders in seconds'
36+
})
37+
38+
register.registerMetric(activeRenders)
39+
register.registerMetric(totalRenders)
40+
register.registerMetric(renderDuration)
41+
42+
let requestCounter = 0
43+
2044
const app = express()
45+
app.set('trust proxy', true)
2146
app.engine('pug', pug.__express)
2247

48+
app.get('/metrics', async (req, res) => {
49+
res.set('Content-Type', register.contentType)
50+
res.end(await register.metrics())
51+
})
52+
2353
if (app.settings.env == 'development')
2454
app.use(require('morgan')('dev'))
2555

@@ -37,25 +67,65 @@ app.use((req, res, next) => {
3767
if (!langs.includes(lang)) lang = 'en'
3868
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
3969

40-
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang, isHead: req.method === 'HEAD' }, (err, resp) => {
41-
if (err) return next(err)
42-
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect.substr(1))
43-
if (resp.errorCode) {
44-
console.error(`Failed with code ${resp.errorCode}:`, resp)
45-
return res.sendStatus(resp.errorCode)
70+
if (typeof process.send === 'function') {
71+
const requestId = ++requestCounter
72+
process.send({ type: 'startRender', requestId })
73+
let responded = false
74+
const handler = (msg) => {
75+
if (msg.requestId === requestId && !responded) {
76+
responded = true
77+
clearTimeout(timeout)
78+
process.removeListener('message', handler)
79+
if (msg.type === 'renderAllowed') {
80+
doRender()
81+
} else if (msg.type === 'renderDenied') {
82+
res.status(503).send('Server overloaded')
83+
}
84+
}
4685
}
86+
process.on('message', handler)
87+
const timeout = setTimeout(() => {
88+
if (!responded) {
89+
responded = true
90+
process.removeListener('message', handler)
91+
console.error('IPC timeout for request', requestId)
92+
res.status(500).send('Internal server error')
93+
}
94+
}, 5000) // 5 second timeout
95+
} else {
96+
doRender()
97+
}
4798

48-
res.status(resp.status || 200)
49-
res.render(indexView, {
50-
prerender_title: resp.title
51-
, prerender_html: resp.html
52-
, canon_url: canonBase ? canonBase + req.url : null
53-
, noscript: true
54-
, theme
55-
, t: l10n[lang]
56-
})
57-
})
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+
}
58117

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+
})
127+
})
128+
}
59129
})
60130

61131
// Cleanup socket file from previous executions

0 commit comments

Comments
 (0)