Skip to content

Commit

Permalink
⚡️ Deduplicate Backend.getOps() calls
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alecgibson committed Aug 27, 2024
1 parent 297ce5d commit 87aa8e3
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 1 deletion.
7 changes: 6 additions & 1 deletion lib/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__') {
Expand Down
17 changes: 17 additions & 0 deletions test/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Check failure on line 107 in test/backend.js

View workflow job for this annotation

GitHub Actions / Node 18

Missing semicolon

Check failure on line 107 in test/backend.js

View workflow job for this annotation

GitHub Actions / Node 20

Missing semicolon

Check failure on line 107 in test/backend.js

View workflow job for this annotation

GitHub Actions / Node 22

Missing semicolon

backend.getOps(agent, 'books', '1984', 0, null, callback);
backend.getOps(agent, 'books', '1984', 0, null, callback);
});
});

describe('getOpsBulk', function() {
Expand Down

0 comments on commit 87aa8e3

Please sign in to comment.