Pmiazga has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/392932 )
Change subject: Abort render on connection close ...................................................................... Abort render on connection close When the client closes the connection: - if the task is still waiting in the queue, remove it; - if the task has already started, abort render. Bug: T180604 Change-Id: I47f6847948ed8903c54fdaaf4fcb5ff021d46c76 --- M lib/queue.js M package.json M routes/html2pdf-v1.js M test/lib/queue.js 4 files changed, 129 insertions(+), 15 deletions(-) Approvals: Pmiazga: Verified; Looks good to me, approved Jdlrobson: Looks good to me, but someone else must approve diff --git a/lib/queue.js b/lib/queue.js index ff437ac..525f75b 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -2,7 +2,6 @@ const asyncQueue = require('async/queue'); const asyncTimeout = require('async/timeout'); -const uuid = require('cassandra-uuid'); // Errors used as the first argument of the callback passed to the queue const callbackErrors = { @@ -79,14 +78,13 @@ const queue = this._queueObject; const timeout = this._options.queueTimeout; - data._id = `${uuid.TimeUuid.now().toString()}|${data.uri}`; data._timeoutID = setTimeout(() => { queue.remove((worker) => { - if (worker.data._id === data._id) { + if (worker.data.id === data.id) { logger.log( 'warn/queue', `Queue is still busy after waiting ` + - `for ${timeout} secs. Data ID: ${data._id}.` + `for ${timeout} secs. Data ID: ${data.id}.` ); callback(callbackErrors.queueBusy, null); return true; @@ -123,11 +121,12 @@ callback(callbackErrors.queueFull, null); return; } + // make sure to cancel the task if it doesn't start within a timeframe this._setCancelTaskTimeout(data, callback); const queueSize = this._countJobsInQueue(); this._logger.log( - 'debug/queue', `Job ${data._id} added to the queue. Jobs waiting: ${queueSize}` + 'debug/queue', `Job ${data.id} added to the queue. Jobs waiting: ${queueSize}` ); this._queueObject.push(data, callback); } @@ -143,21 +142,20 @@ */ _worker(data, callback) { this._clearCancelTaskTimeout(data); - this._logger.log('info/queue', `Started rendering ${data._id}`); + this._logger.log('info/queue', `Started rendering ${data.id}`); const timeout = this._options.executionTimeout; const timedRender = asyncTimeout(this._render.bind(this), timeout * 1000); - const renderer = data._renderer; timedRender(data, (error, pdf) => { // error returned by async timeout if (error && error.code === 'ETIMEDOUT') { this._logger.log( 'error/render', `Aborting. Render hasn't finished within ${timeout} ` + - `seconds. Data ID: ${data._id}.` + `seconds. Data ID: ${data.id}.` ); - renderer.abortRender(); + data.renderer.abortRender(); callback(callbackErrors.renderTimeout, null); } else { callback(error, pdf); @@ -171,23 +169,53 @@ * @param {Function} callback called on render success/failure */ _render(data, callback) { - data._renderer + data.renderer .articleToPdf(data.uri, data.format, this._puppeteerOptions, this._pdfOptions) .then((pdf) => { this._logger.log( - 'debug/queue', `Job ${data._id} rendered successfully` + 'debug/queue', `Job ${data.id} rendered successfully` ); callback(null, pdf); }) .catch((error) => { this._logger.log('error/render', { - msg: `Cannot convert page ${data.uri} to PDF. Error: ${error.toString()}`, + msg: `Data ID: ${data.id} to PDF. ${error.toString()}`, error }); callback(callbackErrors.renderFailed, null); }); } + + /** + * Abort task identified by `id` + * @param {string} id ID initially passed as part of data + * @param {Renderer} renderer instance of Renderer + */ + abort(id, renderer) { + let taskStarted = true; + + // has the task started already? + this._queueObject.remove((worker) => { + if (worker.data.id === id) { + this._logger.log( + 'debug/queue', + `Removing task from the queue. Data ID: ${id}.` + ); + taskStarted = false; + return true; + } + return false; + }); + + if (taskStarted) { + this._logger.log( + 'debug/render', + `Aborting render. Data ID: ${id}.` + ); + renderer.abortRender(); + } + } } module.exports = { diff --git a/package.json b/package.json index c2dd183..9f2449e 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,13 @@ "compression": "^1.7.1", "domino": "^1.0.30", "express": "^4.16.2", + "http-shutdown": "^1.2.0", "js-yaml": "^3.10.0", "preq": "^0.5.3", "puppeteer": "^0.11.0", "service-runner": "^2.4.2", "swagger-router": "^0.7.1", - "swagger-ui": "git+https://github.com/wikimedia/swagger-ui#master", - "http-shutdown": "^1.2.0" + "swagger-ui": "git+https://github.com/wikimedia/swagger-ui#master" }, "devDependencies": { "extend": "^3.0.1", diff --git a/routes/html2pdf-v1.js b/routes/html2pdf-v1.js index f1993e0..73dae5e 100644 --- a/routes/html2pdf-v1.js +++ b/routes/html2pdf-v1.js @@ -2,6 +2,7 @@ const { callbackErrors, Queue } = require('../lib/queue'); const sUtil = require('../lib/util'); +const uuid = require('cassandra-uuid'); const Renderer = require('../lib/renderer'); /** @@ -27,8 +28,10 @@ } }); + const id = `${uuid.TimeUuid.now().toString()}|${restbaseRequest.uri}`; const renderer = new Renderer(); app.queue.push({ + id, renderer, uri: restbaseRequest.uri, format: req.params.format @@ -60,6 +63,15 @@ res.writeHead(200, headers); res.end(pdf, 'binary'); })); + + req.on('close', () => { + app.logger.log( + 'debug/request', + `Connection closed by the client. ` + + `Will try and cancel the task with ID ${id}.` + ); + app.queue.abort(id, renderer); + }); }); module.exports = function(appObj) { diff --git a/test/lib/queue.js b/test/lib/queue.js index 3d63bad..91a1cf6 100644 --- a/test/lib/queue.js +++ b/test/lib/queue.js @@ -24,7 +24,7 @@ } }; -describe('concurrency', function() { +describe('Queue', function() { this.timeout(5000); it('should run only one worker at a time', function(done) { @@ -201,4 +201,78 @@ done(); }); }); + + it('should remove task from queue when task is aborted', function(done) { + class QueueTest extends Queue { + _render(data, callback) { + // simulate render + setTimeout(() => { + callback(null, {}); + }, data.timeout); + } + } + const q = new QueueTest({ + concurrency: 1, + queueTimeout: 30, + executionTimeout: 10, + maxTaskCount: 10 + }, puppeteerFlags, pdfOptions, logger); + + q.push({ + id: 1, + renderer, + timeout: 1000 + }, (error, data) => { + assert.ok(error === null, + 'Render finished.'); + assert.ok(q._countJobsInQueue() === 0, + 'Queue is empty.'); + done(); + }); + + q.push({ + id: 2, + renderer, + timeout: 500 + }, (error, data) => { + assert(false, 'Callback should never be called as the job has ' + + 'been removed from the queue.'); + }); + q.abort(2); + }); + + it('should abort render when task is aborted', function(done) { + class QueueTest extends Queue { + _render(data, callback) { + // simulate render + setTimeout(() => { + callback(null, {}); + }, data.timeout); + } + } + const q = new QueueTest({ + concurrency: 1, + queueTimeout: 30, + executionTimeout: 10, + maxTaskCount: 10 + }, puppeteerFlags, pdfOptions, logger); + const renderer = { + abortRender: () => { + assert.ok(true, 'Renderer abort is called.'); + done(); + } + }; + + q.push({ + id: 1, + renderer, + timeout: 5000 + }, (error, data) => {}); + + // wait a little for the task to start + setTimeout(() => { + q.abort(1, renderer); + }, 20); + }); + }); -- To view, visit https://gerrit.wikimedia.org/r/392932 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I47f6847948ed8903c54fdaaf4fcb5ff021d46c76 Gerrit-PatchSet: 4 Gerrit-Project: mediawiki/services/chromium-render Gerrit-Branch: master Gerrit-Owner: Bmansurov <bmansu...@wikimedia.org> Gerrit-Reviewer: Bmansurov <bmansu...@wikimedia.org> Gerrit-Reviewer: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: Mobrovac <mobro...@wikimedia.org> Gerrit-Reviewer: Phuedx <samsm...@wikimedia.org> Gerrit-Reviewer: Pmiazga <pmia...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits