Skip to content

Commit 28abae1

Browse files
committed
feat(prerenderer): add prom + 503 overload protection
1 parent 46af06a commit 28abae1

File tree

3 files changed

+87
-18
lines changed

3 files changed

+87
-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 = numCPUs * 2
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--
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: 68 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,35 @@ 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+
2042
const app = express()
2143
app.engine('pug', pug.__express)
2244

45+
app.get('/metrics', async (req, res) => {
46+
res.set('Content-Type', register.contentType)
47+
res.end(await register.metrics())
48+
})
49+
2350
if (app.settings.env == 'development')
2451
app.use(require('morgan')('dev'))
2552

@@ -37,25 +64,50 @@ app.use((req, res, next) => {
3764
if (!langs.includes(lang)) lang = 'en'
3865
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
3966

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)
67+
if (typeof process.send === 'function') {
68+
const requestId = Math.random().toString(36)
69+
process.send({ type: 'startRender', requestId })
70+
const handler = (msg) => {
71+
if (msg.requestId === requestId) {
72+
process.removeListener('message', handler)
73+
if (msg.type === 'renderAllowed') {
74+
doRender()
75+
} else if (msg.type === 'renderDenied') {
76+
res.status(503).send('Server overloaded')
77+
}
78+
}
4679
}
80+
process.on('message', handler)
81+
} else {
82+
doRender()
83+
}
4784

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-
})
85+
function doRender() {
86+
activeRenders.inc()
87+
const end = renderDuration.startTimer()
88+
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang, isHead: req.method === 'HEAD' }, (err, resp) => {
89+
if (typeof process.send === 'function') process.send({ type: 'endRender' })
90+
activeRenders.dec()
91+
end()
92+
totalRenders.inc()
93+
if (err) return next(err)
94+
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect.substr(1))
95+
if (resp.errorCode) {
96+
console.error(`Failed with code ${resp.errorCode}:`, resp)
97+
return res.sendStatus(resp.errorCode)
98+
}
5899

100+
res.status(resp.status || 200)
101+
res.render(indexView, {
102+
prerender_title: resp.title
103+
, prerender_html: resp.html
104+
, canon_url: canonBase ? canonBase + req.url : null
105+
, noscript: true
106+
, theme
107+
, t: l10n[lang]
108+
})
109+
})
110+
}
59111
})
60112

61113
// Cleanup socket file from previous executions

0 commit comments

Comments
 (0)