Bmansurov has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/390144 )
Change subject: Terminate browser process after certain time ...................................................................... Terminate browser process after certain time By default after 5 minutes the puppeteer will terminate the browser subprocess if it hasn't finished returning a PDF yet. Bug: T178501 Change-Id: I904fc15826b7de3a34e5c977b40bfd8518bb1679 --- M config.dev.yaml M lib/queue.js M lib/renderer.js M routes/html2pdf-v1.js M test/lib/queue.js A test/lib/renderer.js 6 files changed, 132 insertions(+), 46 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/chromium-render refs/changes/44/390144/1 diff --git a/config.dev.yaml b/config.dev.yaml index 6b5b682..00d6f4d 100644 --- a/config.dev.yaml +++ b/config.dev.yaml @@ -91,12 +91,17 @@ # some room for page numbers bottom: '0.75in' left: '0.5in' - puppeteer_flags: - - '--no-sandbox' - - '--disable-setuid-sandbox' + # https://github.com/GoogleChrome/puppeteer/blob/v0.11.0/docs/api.md#puppeteerlaunchoptions + puppeteer_options: + timeout: 30000 + args: + - '--no-sandbox' + - '--disable-setuid-sandbox' # the maximum number of puppeteer instances that can be launched at a time render_concurrency: 1 # don't wait to render a PDF after this many seconds render_queue_timeout: 90 # maximum allowed number of pending jobs - render_queue_count: 3 \ No newline at end of file + render_queue_count: 3 + # the number of seconds before puppeteer terminates the browser instance + render_execution_timout: 300 \ No newline at end of file diff --git a/lib/queue.js b/lib/queue.js index 4fcea12..1750858 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -19,22 +19,24 @@ class Queue { /** * @param {number} concurrency number of concurrent render instances - * @param {number} timeout number of seconds after which the + * @param {number} queueTimeout number of seconds after which the * yet-to-start renders are aborted * @param {number} maxCount number of tasks the queue should hold. * New tasks will be rejected once the number of tasks in the queue * is equal to this number. - * @param {Object} puppeteerFlags flags used to in starting puppeteer + * @param {number} executionTimeout number of seconds after which puppeteer + * closes the browser + * @param {Object} puppeteerOptions options used to in starting puppeteer * @param {Object} pdfOptions pdf options passed to Chromium * @param {Object} logger app logger */ - constructor( - concurrency, timeout, maxCount, puppeteerFlags, pdfOptions, logger - ) { + constructor(concurrency, queueTimeout, maxCount, executionTimeout, + puppeteerOptions, pdfOptions, logger) { this._queueObject = asyncQueue(this._worker.bind(this), concurrency); - this._timeout = timeout; + this._queueTimeout = queueTimeout; this._maxCount = maxCount; - this._puppeteerFlags = puppeteerFlags; + this._executionTimeout = executionTimeout; + this._puppeteerOptions = puppeteerOptions; this._pdfOptions = pdfOptions; this._logger = logger; } @@ -67,14 +69,14 @@ if (worker.data._id === data._id) { that._logger.log('trace/info', { msg: `Queue is still busy after waiting ` + - `for ${that._timeout} secs.` + `for ${that._queueTimeout} secs.` }); callback(queueErrors.timeout, null); return true; } return false; }); - }, this._timeout * 1000); + }, this._queueTimeout * 1000); q.push(data, callback); } @@ -85,8 +87,8 @@ clearTimeout(data._timeoutID); renderer - .articleToPdf(data.uri, data.format, this._puppeteerFlags, - this._pdfOptions) + .articleToPdf(data.uri, data.format, this._puppeteerOptions, + this._pdfOptions, this._executionTimeout) .then((pdf) => { callback(null, pdf); }) diff --git a/lib/renderer.js b/lib/renderer.js index dae34f1..518f849 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -6,36 +6,51 @@ * Renders content from `url` in PDF * @param {string} url URL to get content from * @param {string} format Page size, e.g. Letter or A4, passed to understands - * @param {Object} puppeteerFlags + * @param {Object} puppeteerOptions * @param {Object} pdfOptions + * @param {number} executionTimeout number of seconds after which puppeteer + * closes the browser * @return {<Promise<Buffer>>} Promise which resolves with PDF buffer */ -exports.articleToPdf = function articleToPdf(url, format, puppeteerFlags, pdfOptions) { +exports.articleToPdf = function articleToPdf( + url, format, puppeteerOptions, pdfOptions, executionTimeout +) { let browser; let page; - return puppeteer.launch({ args: puppeteerFlags }) - .then((browser_) => { - browser = browser_; - return browser.newPage(); - }) - .then((page_) => { - page = page_; - return page.goto(url, { waitUntil: 'networkidle' }); - }) - .then(() => { - return page.pdf(Object.assign( - {}, pdfOptions, { format } - )); - }) - .catch((error) => { + return new Promise((resolve, reject) => { + const timeoutID = setTimeout(() => { if (browser) { browser.close(); } - throw error; - }) - .then((pdf) => { - browser.close(); - return pdf; - }); + reject('timeout'); + }, executionTimeout * 1000); + + puppeteer.launch(puppeteerOptions) + .then((browser_) => { + browser = browser_; + return browser.newPage(); + }) + .then((page_) => { + page = page_; + return page.goto(url, { waitUntil: 'networkidle' }); + }) + .then(() => { + return page.pdf(Object.assign( + {}, pdfOptions, { format } + )); + }) + .catch((error) => { + clearTimeout(timeoutID); + if (browser) { + browser.close(); + } + reject(error); + }) + .then((pdf) => { + clearTimeout(timeoutID); + browser.close(); + resolve(pdf); + }); + }); }; diff --git a/routes/html2pdf-v1.js b/routes/html2pdf-v1.js index 17d7766..5847f48 100644 --- a/routes/html2pdf-v1.js +++ b/routes/html2pdf-v1.js @@ -65,7 +65,8 @@ conf.render_concurrency, conf.render_queue_timeout, conf.render_queue_count, - conf.puppeteer_flags, + conf.render_execution_timout, + conf.puppeteer_options, conf.pdf_options, app.logger ); diff --git a/test/lib/queue.js b/test/lib/queue.js index db0e26e..aeb5422 100644 --- a/test/lib/queue.js +++ b/test/lib/queue.js @@ -3,10 +3,13 @@ const assert = require('../utils/assert.js'); const { queueErrors, Queue } = require('../../lib/queue'); const logger = { log: () => {} }; -const puppeteerFlags = [ - '--no-sandbox', - '--disable-setuid-sandbox' -]; +const puppeteerOptions = { + timeout: 3000, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox' + ] +}; const pdfOptions = { scale: 1, displayHeaderFooter: false, @@ -39,7 +42,7 @@ }, data.timeout); }; } - const q = new QueueTest(1, 90, 10, puppeteerFlags, pdfOptions, logger); + const q = new QueueTest(1, 90, 10, 300, puppeteerOptions, pdfOptions, logger); // first worker must finish after 1 sec q.push({ @@ -82,7 +85,7 @@ }, data.timeout); }; } - const q = new QueueTest(1, 5, 1, puppeteerFlags, pdfOptions, logger); + const q = new QueueTest(1, 5, 1, 300, puppeteerOptions, pdfOptions, logger); // first worker completes in 3 seconds q.push({ @@ -119,7 +122,7 @@ }, data.timeout); }; } - const q = new QueueTest(1, 1, 10, puppeteerFlags, pdfOptions, logger); + const q = new QueueTest(1, 1, 10, 300, puppeteerOptions, pdfOptions, logger); // first worker completes in 3 seconds q.push({ diff --git a/test/lib/renderer.js b/test/lib/renderer.js new file mode 100644 index 0000000..b778c71 --- /dev/null +++ b/test/lib/renderer.js @@ -0,0 +1,60 @@ +'use strict'; + +const preq = require('preq'); +const assert = require('../utils/assert.js'); +const renderer = require('../../lib/renderer'); + +const logger = { log: () => {} }; +const puppeteerOptions = { + timeout: 3000, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox' + ] +}; +const pdfOptions = { + scale: 1, + displayHeaderFooter: false, + printBackground: false, + landscape: false, + pageRanges: '', + format: 'Letter', + margin: { + top: '0.5in', + right: '0.5in', + // some room for page numbers + bottom: '0.75in', + left: '0.5in' + } +}; +const baseUrl = 'https://en.wikipedia.org/api/rest_v1/page/html/'; + +describe('renderer', function() { + it('should return a promise that resolves with PDF', function() { + return renderer.articleToPdf( + `${baseUrl}Book_(disambiguation)`, + 'Letter', + puppeteerOptions, + pdfOptions, + 300 + ).then((pdf) => { + assert.ok(Buffer.isBuffer(pdf)); + }).catch((error) => { + assert.ok(false, `Unexepected error ${error}`); + });; + }); + + it('should reject when there is not enough time to render PDF', function() { + return renderer.articleToPdf( + `${baseUrl}Book_(disambiguation)`, + 'Letter', + puppeteerOptions, + pdfOptions, + 0.1 + ).then((pdf) => { + assert.ok(false, 'Was not expecting a PDF'); + }).catch((error) => { + assert.ok(error === 'timeout'); + }); + }); +}); -- To view, visit https://gerrit.wikimedia.org/r/390144 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I904fc15826b7de3a34e5c977b40bfd8518bb1679 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/services/chromium-render Gerrit-Branch: master Gerrit-Owner: Bmansurov <bmansu...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits