From 4d856dd87238f80fd648d05565d9b877023feee6 Mon Sep 17 00:00:00 2001 From: Joshua Kiwiet-Pantaleoni Date: Mon, 8 Jan 2024 19:25:42 -0800 Subject: [PATCH] feat: use pool of worker threads for rendering --- server/vue-middleware.js | 68 ++++++++++++------------- server/vue-worker.js | 107 +++++++++++++++++++++------------------ 2 files changed, 90 insertions(+), 85 deletions(-) diff --git a/server/vue-middleware.js b/server/vue-middleware.js index dc24bf91458..875bcb7b957 100644 --- a/server/vue-middleware.js +++ b/server/vue-middleware.js @@ -1,9 +1,9 @@ const fs = require('fs'); const path = require('path'); -const { Worker, SHARE_ENV } = require('worker_threads'); +const { SHARE_ENV } = require('worker_threads'); +const workerpool = require('workerpool'); const Bowser = require('bowser'); const cookie = require('cookie'); -const log = require('./util/log'); const protectedRoutes = require('./util/protectedRoutes.js'); const tracer = require('./util/ddTrace'); @@ -40,6 +40,20 @@ module.exports = function createMiddleware({ // eslint-disable-next-line no-param-reassign clientManifest.publicPath = config.app.publicPath || '/'; + // Create a worker pool to render the app + const pool = workerpool.pool(path.resolve(__dirname, 'vue-worker.js'), { + workerType: 'thread', + workerThreadOpts: { + workerData: { + clientManifest, + serverBundle, + serverConfig: config.server, + template, + }, + env: SHARE_ENV, + }, + }); + function middleware(req, res, next) { const cookies = cookie.parse(req.headers.cookie || ''); const userAgent = req.get('user-agent'); @@ -65,42 +79,22 @@ module.exports = function createMiddleware({ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } - // Create a worker thread to render the app - const worker = new Worker(path.resolve(__dirname, 'vue-worker.js'), { - workerData: { - clientManifest, - context, - serverBundle, - serverConfig: config.server, - template, - }, - env: SHARE_ENV, - }); - - // Send the rendered html back to the client - worker.on('message', ({ error, html, setCookies }) => { - // set any cookies created during the app render - setCookies.forEach(setCookie => res.append('Set-Cookie', setCookie)); - - if (error) { - handleError(error, req, res, next); - } else { - // send the final rendered html - res.send(html); - } - }); - - // Handle any errors that occur in the worker - worker.on('error', err => { - handleError(err, req, res, next); - }); + // render the app using the worker pool + pool.exec('render', [context]) + .then(({ error, html, setCookies }) => { + // set any cookies created during the app render + setCookies.forEach(setCookie => res.append('Set-Cookie', setCookie)); - // Handle the worker exiting - worker.on('exit', code => { - if (code !== 0) { - log.error(new Error(`Worker stopped with exit code ${code}`)); - } - }); + if (error) { + handleError(error, req, res, next); + } else { + // send the final rendered html + res.send(html); + } + }) + .catch(err => { + handleError(err, req, res, next); + }); } return tracer.wrap('vue-middleware', middleware); diff --git a/server/vue-worker.js b/server/vue-worker.js index e3e13c5f539..2deec9abd5b 100644 --- a/server/vue-worker.js +++ b/server/vue-worker.js @@ -1,5 +1,6 @@ -const { parentPort, workerData } = require('worker_threads'); const { createBundleRenderer } = require('vue-server-renderer'); +const { workerData } = require('worker_threads'); +const workerpool = require('workerpool'); const initCache = require('./util/initCache'); const log = require('./util/log'); const vueSsrCache = require('./util/vueSsrCache'); @@ -8,14 +9,12 @@ const getSessionCookies = require('./util/getSessionCookies'); const { clientManifest, - context, serverBundle, serverConfig, template, } = workerData; const isProd = process.env.NODE_ENV === 'production'; -const s = Date.now(); const cache = initCache(serverConfig); // create a new renderer instance @@ -29,52 +28,64 @@ const renderer = createBundleRenderer(serverBundle, { shouldPrefetch: () => false, }); -// get graphql api possible types for the graphql client -const typesPromise = getGqlPossibleTypes(serverConfig.graphqlUri, cache) - .finally(() => { - if (!isProd) { - log.info(`fragment fetch: ${Date.now() - s}ms`); - } - }); +async function render(context) { + const s = Date.now(); -// fetch initial session cookies in case starting session with this request -const cookiePromise = getSessionCookies(serverConfig.sessionUri, context.cookies) - .finally(() => { - if (!isProd) { - log.info(`session fetch: ${Date.now() - s}ms`); - } - }); - -const setCookies = []; + // get graphql api possible types for the graphql client + const typesPromise = getGqlPossibleTypes(serverConfig.graphqlUri, cache) + .finally(() => { + if (!isProd) { + log.info(`fragment fetch: ${Date.now() - s}ms`); + } + }); -Promise.all([typesPromise, cookiePromise]) - .then(([types, cookieInfo]) => { - // add fetched types to rendering context - context.config.graphqlPossibleTypes = types; - // update cookies in the rendering context with any newly fetched session cookies - context.cookies = Object.assign(context.cookies, cookieInfo.cookies); - // forward any newly fetched 'Set-Cookie' headers - context.setCookies = [...cookieInfo.setCookies]; - // render the app - return renderer.renderToString(context); - }).then(html => { - // collect any cookies created during the app render - setCookies.concat(context.setCookies); - // send the final rendered html - parentPort.postMessage({ - html, - setCookies, + // fetch initial session cookies in case starting session with this request + const cookiePromise = getSessionCookies(serverConfig.sessionUri, context.cookies) + .finally(() => { + if (!isProd) { + log.info(`session fetch: ${Date.now() - s}ms`); + } }); - if (!isProd) { - log.info(`whole request: ${Date.now() - s}ms`); - } - }).catch(err => { - // collect any cookies created during the app render - setCookies.concat(context.setCookies); - // send the error - parentPort.postMessage({ - error: err, - // only send cookies if there is a redirect url - setCookies: err.url ? setCookies : [], + + const setCookies = []; + + return Promise.all([typesPromise, cookiePromise]) + .then(([types, cookieInfo]) => { + // add fetched types to rendering context + context.config.graphqlPossibleTypes = types; + // update cookies in the rendering context with any newly fetched session cookies + context.cookies = Object.assign(context.cookies, cookieInfo.cookies); + // forward any newly fetched 'Set-Cookie' headers + context.setCookies = [...cookieInfo.setCookies]; + // render the app + return renderer.renderToString(context); + }) + .then(html => { + // collect any cookies created during the app render + setCookies.concat(context.setCookies); + // send the final rendered html + return { + html, + setCookies, + }; + }) + .catch(err => { + // collect any cookies created during the app render + setCookies.concat(context.setCookies); + // send the error + return { + error: err, + // only send cookies if there is a redirect url + setCookies: err.url ? setCookies : [], + }; + }) + .finally(() => { + if (!isProd) { + log.info(`whole request: ${Date.now() - s}ms`); + } }); - }); +} + +workerpool.worker({ + render, +});