Skip to content

Commit a88011a

Browse files
committed
feat(prerenderer): add prom + 503 overload protection
1 parent 54a8453 commit a88011a

File tree

3 files changed

+104
-18
lines changed

3 files changed

+104
-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: 85 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,37 @@ 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()
2145
app.engine('pug', pug.__express)
2246

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

@@ -37,25 +66,65 @@ app.use((req, res, next) => {
3766
if (!langs.includes(lang)) lang = 'en'
3867
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
3968

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)
69+
if (typeof process.send === 'function') {
70+
const requestId = ++requestCounter
71+
process.send({ type: 'startRender', requestId })
72+
let responded = false
73+
const handler = (msg) => {
74+
if (msg.requestId === requestId && !responded) {
75+
responded = true
76+
clearTimeout(timeout)
77+
process.removeListener('message', handler)
78+
if (msg.type === 'renderAllowed') {
79+
doRender()
80+
} else if (msg.type === 'renderDenied') {
81+
res.status(503).send('Server overloaded')
82+
}
83+
}
4684
}
85+
process.on('message', handler)
86+
const timeout = setTimeout(() => {
87+
if (!responded) {
88+
responded = true
89+
process.removeListener('message', handler)
90+
console.error('IPC timeout for request', requestId)
91+
res.status(500).send('Internal server error')
92+
}
93+
}, 5000) // 5 second timeout
94+
} else {
95+
doRender()
96+
}
4797

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-
})
98+
function doRender() {
99+
activeRenders.inc()
100+
const end = renderDuration.startTimer()
101+
let metricsUpdated = false
102+
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang, isHead: req.method === 'HEAD' }, (err, resp) => {
103+
if (!metricsUpdated) {
104+
metricsUpdated = true
105+
if (typeof process.send === 'function') process.send({ type: 'endRender' })
106+
activeRenders.dec()
107+
end()
108+
totalRenders.inc()
109+
}
110+
if (err) return next(err)
111+
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect.substr(1))
112+
if (resp.errorCode) {
113+
console.error(`Failed with code ${resp.errorCode}:`, resp)
114+
return res.sendStatus(resp.errorCode)
115+
}
58116

117+
res.status(resp.status || 200)
118+
res.render(indexView, {
119+
prerender_title: resp.title
120+
, prerender_html: resp.html
121+
, canon_url: canonBase ? canonBase + req.url : null
122+
, noscript: true
123+
, theme
124+
, t: l10n[lang]
125+
})
126+
})
127+
}
59128
})
60129

61130
// Cleanup socket file from previous executions

0 commit comments

Comments
 (0)