From e81522f16d0a3621bf1847c7bb8f51ec6d1954ee Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:24:56 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Deduplicate=20`Backend?= =?UTF-8?q?.getOps()`=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls to `Backend.getOps()` can be quite expensive. In order to minimize the impact of these calls on the server, this change deduplicates concurrent calls with the same arguments, and makes only a single call, then invoking all the callbacks with the single result. --- lib/backend.js | 7 ++++++- lib/util.js | 29 +++++++++++++++++++++++++++++ test/backend.js | 17 +++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index 7a3cd41cc..7676daaa0 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -57,6 +57,11 @@ function Backend(options) { function(error, context) { logger.error(error); }; + + var backend = this; + this._dbGetOps = util.deduplicateRequests(function() { + backend.db.getOps.apply(backend.db, arguments); + }); } module.exports = Backend; emitter.mixin(Backend); @@ -324,7 +329,7 @@ Backend.prototype._getSanitizedOps = function(agent, projection, collection, id, var backend = this; if (!opsOptions) opsOptions = {}; if (agent) opsOptions.agentCustom = agent.custom; - backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) { + this._dbGetOps(collection, id, from, to, opsOptions, function(err, ops) { if (err) return callback(err); backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { if (err) return callback(err); diff --git a/lib/util.js b/lib/util.js index 0d613ed58..8682fc2ad 100644 --- a/lib/util.js +++ b/lib/util.js @@ -98,6 +98,35 @@ exports.clone = function(obj) { return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj)); }; +exports.deduplicateRequests = function(fn) { + var callbacksByArgs = {}; + return function() { + var callback = arguments[arguments.length - 1]; + var args = []; + for (var i = 0; i < arguments.length - 1; i++) { + args.push(arguments[i]); + } + var argString = JSON.stringify(args); + + var callbacks = exports.digOrCreate(callbacksByArgs, argString, function() { + return []; + }); + callbacks.push(callback); + + if (callbacks.length > 1) return; + + args.push(function() { + while (callbacks.length) { + var cb = callbacks.shift(); + cb.apply(null, arguments); + } + delete callbacksByArgs[argString]; + }); + + fn.apply(null, args); + }; +}; + var objectProtoPropNames = Object.create(null); Object.getOwnPropertyNames(Object.prototype).forEach(function(prop) { if (prop !== '__proto__') { diff --git a/test/backend.js b/test/backend.js index e0af12565..883bc4105 100644 --- a/test/backend.js +++ b/test/backend.js @@ -92,6 +92,23 @@ describe('Backend', function() { done(); }); }); + + it('deduplicates concurrent requests', function(done) { + var getOps = sinon.spy(backend.db, 'getOps'); + var count = 0; + var callback = function(error, ops) { + if (error) return done(error); + expect(ops).to.have.length(2); + expect(ops[0].create.data).to.eql({title: '1984'}); + expect(ops[1].op).to.eql([{p: ['author'], oi: 'George Orwell'}]); + count++; + expect(getOps).to.have.been.calledOnce; + if (count === 2) done(); + }; + + backend.getOps(agent, 'books', '1984', 0, null, callback); + backend.getOps(agent, 'books', '1984', 0, null, callback); + }); }); describe('getOpsBulk', function() { From 599d175142200e47bb25b4893aad279f3f3f647f Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:55:02 +0100 Subject: [PATCH 2/2] Review markups - Clean up `arguments` -> `Array` --- lib/util.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/util.js b/lib/util.js index 8682fc2ad..f5ad1889f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -101,11 +101,8 @@ exports.clone = function(obj) { exports.deduplicateRequests = function(fn) { var callbacksByArgs = {}; return function() { - var callback = arguments[arguments.length - 1]; - var args = []; - for (var i = 0; i < arguments.length - 1; i++) { - args.push(arguments[i]); - } + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); var argString = JSON.stringify(args); var callbacks = exports.digOrCreate(callbacksByArgs, argString, function() {