Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions prerender-server/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions prerender-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
"dependencies": {
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"superagent": "^6.1.0",
"prom-client": "^14.0.0"
"express-queue": "^0.0.13",
"prom-client": "^14.0.0",
"superagent": "^6.1.0"
},
"scripts": {
"start": "./start.sh",
Expand Down
2 changes: 1 addition & 1 deletion prerender-server/src/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);

let activeRenders = 0
const MAX_TOTAL_RENDERS = parseInt(process.env.MAX_TOTAL_RENDERS || '1')
const MAX_TOTAL_RENDERS = parseInt(process.env.MAX_TOTAL_RENDERS || numCPUs.toString())

for (let i = 0; i < numCPUs; i++) {
cluster.fork()
Expand Down
89 changes: 56 additions & 33 deletions prerender-server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import promClient from 'prom-client'
import l10n from '../client/l10n'
import render from '../client/run-server'

if (!process.env.API_URL) {
throw new Error('API_URL environment variable is required but not defined.');
}

const themes = ['light', 'dark']
, langs = Object.keys(l10n)
, baseHref = process.env.BASE_HREF || '/'
Expand Down Expand Up @@ -56,17 +60,30 @@ if (app.settings.env == 'development')
app.use(require('cookie-parser')())
app.use(require('body-parser').urlencoded({ extended: false }))

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

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

let lang = req.query.lang || req.cookies.lang || 'en'
if (!langs.includes(lang)) lang = 'en'
if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang)
req.renderOpts = { theme, lang }
next()
})

if (queue) app.use(queue)

app.use((req, res, next) => {
// TODO: optimize /block-height/nnn (no need to render the whole app just to get the redirect)

// IPC-based queuing for cluster mode
if (typeof process.send === 'function') {
const requestId = ++requestCounter
process.send({ type: 'startRender', requestId })
Expand All @@ -77,8 +94,9 @@ app.use((req, res, next) => {
clearTimeout(timeout)
process.removeListener('message', handler)
if (msg.type === 'renderAllowed') {
doRender()
doRender(req, res, next)
} else if (msg.type === 'renderDenied') {
// received when the master's render queue is full
res.status(503).send('Server overloaded')
}
}
Expand All @@ -93,40 +111,45 @@ app.use((req, res, next) => {
}
}, 5000) // 5 second timeout
} else {
doRender()
// standalone mode
doRender(req, res, next)
}
})

function doRender() {
activeRenders.inc()
const end = renderDuration.startTimer()
let metricsUpdated = false
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang, isHead: req.method === 'HEAD' }, (err, resp) => {
if (!metricsUpdated) {
metricsUpdated = true
if (typeof process.send === 'function') process.send({ type: 'endRender' })
activeRenders.dec()
end()
totalRenders.inc()
}
if (err) return next(err)
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect)
if (resp.errorCode) {
console.error(`Failed with code ${resp.errorCode}:`, resp)
return res.sendStatus(resp.errorCode)
}
function doRender(req, res, next) {
activeRenders.inc()
const end = renderDuration.startTimer()
let metricsUpdated = false
render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { ...req.renderOpts, isHead: req.method === 'HEAD' }, (err, resp) => {
if (!metricsUpdated) {
metricsUpdated = true
// inform the master process that we're done rendering and can accept new requests
if (typeof process.send === 'function') process.send({ type: 'endRender' })
// and tell express-queue that we're ready for the next one
if (queue) queue.next()
activeRenders.dec()
end()
totalRenders.inc()
}

res.status(resp.status || 200)
res.render(indexView, {
prerender_title: resp.title
, prerender_html: resp.html
, canon_url: canonBase ? canonBase + req.url : null
, noscript: true
, theme
, t: l10n[lang]
})
if (err) return next(err)
if (resp.redirect) return res.redirect(301, baseHref + resp.redirect)
if (resp.errorCode) {
console.error(`Failed with code ${resp.errorCode}:`, resp)
return res.sendStatus(resp.errorCode)
}

res.status(resp.status || 200)
res.render(indexView, {
prerender_title: resp.title
, prerender_html: resp.html
, canon_url: canonBase ? canonBase + req.url : null
, noscript: true
, ...req.renderOpts
, t: l10n[req.renderOpts.lang]
})
}
})
})
}

// Cleanup socket file from previous executions
if (process.env.SOCKET_PATH) {
Expand Down