[IMP] web: add MutexedDropPrevious in concurrency

This new helper is meant to solve the following use case: serialize a number
of concurrent rpcs while skipping the rpcs that arrive in between the mutex.
This commit is contained in:
Martin Geubelle
2018-08-13 17:09:28 +02:00
committed by Christophe Matthieu
parent 971629b9fe
commit 7f4b4f86bd
2 changed files with 292 additions and 3 deletions
@@ -219,6 +219,72 @@ return {
});
}
}),
/**
* A MutexedDropPrevious is a primitive for serializing computations while
* skipping the ones that where executed between a current one and before
* the execution of a new one. This is useful to avoid useless RPCs.
*
* You can read the Mutex description to understand its role ; for the
* DropPrevious part of this abstraction, imagine the following situation:
* you have a code that call the server with a fixed argument and a list of
* operations that only grows after each call and you only care about the
* RPC result (the server code doesn't do anything). If this code is called
* three times (A B C) and C is executed before B has started, it's useless
* to make an extra RPC (B) if you know that it won't have an impact and you
* won't use its result.
*
* Note that the promise returned by the exec call won't be resolved if
* exec is called before the first exec call resolution ; only the promise
* returned by the last exec call will be resolved (the other are rejected);
*
* A MutexedDropPrevious has to be a class, and not a function, because we
* have to keep track of some internal state. The exec function takes as
* argument an action (and not a deferred as DropPrevious for example)
* because it's the MutexedDropPrevious role to trigger the RPC call that
* returns a deferred when it's time.
*/
MutexedDropPrevious: Class.extend({
init: function () {
this.currentDef = null;
this.locked = false;
this.pendingAction = null;
this.pendingDef = null;
},
/**
* @param {function} action a function which may return a deferred
* @returns {Deferred}
*/
exec: function (action) {
var self = this;
if (this.locked) {
this.pendingAction = action;
var oldPendingDef = this.pendingDef;
var pendingDef = this.pendingDef = $.Deferred();
if (oldPendingDef) {
oldPendingDef.reject();
}
this.currentDef.reject();
return pendingDef.promise();
} else {
this.locked = true;
this.currentDef = $.Deferred();
$.when(action())
.then(this.currentDef.resolve.bind(this.currentDef))
.fail(this.currentDef.reject.bind(this.currentDef))
.always(function () {
self.locked = false;
if (self.pendingAction) {
var action = self.pendingAction;
self.pendingAction = null;
self.exec(action)
.then(self.pendingDef.resolve.bind(self.pendingDef))
.fail(self.pendingDef.reject.bind(self.pendingDef));
}
});
return this.currentDef.promise();
}
},
}),
/**
* Rejects a deferred as soon as a reference deferred is either resolved or
* rejected
@@ -165,7 +165,6 @@ QUnit.module('core', {}, function () {
// d2 is resolved
assert.ok(done2);
assert.ok(!fail2);
});
QUnit.test('DropMisordered: resolve all correctly ordered, async', function (assert) {
@@ -197,9 +196,9 @@ QUnit.module('core', {}, function () {
done1 = false, done2 = false,
fail1 = false, fail2 = false;
var d1 = $.Deferred(),
var d1 = $.Deferred(),
d2 = $.Deferred();
dm.add(d1).done(function () { done1 = true; })
.fail(function () { fail1 = true; });
dm.add(d2).done(function () { done2 = true; })
@@ -251,6 +250,230 @@ QUnit.module('core', {}, function () {
}, 30);
});
QUnit.test('MutexedDropPrevious: simple', function (assert) {
assert.expect(3);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var p1 = m.exec(function () { return d1; }).then(function (result) {
assert.strictEqual(result, 'd1');
});
assert.strictEqual(p1.state(), "pending");
d1.resolve('d1');
assert.strictEqual(p1.state(), "resolved");
});
QUnit.test('MutexedDropPrevious: 2 arrives after 1 resolution', function (assert) {
assert.expect(6);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var p1 = m.exec(function () { return d1; }).then(function (result) {
assert.strictEqual(result, 'd1');
});
assert.strictEqual(p1.state(), "pending");
d1.resolve('d1');
assert.strictEqual(p1.state(), "resolved");
var d2 = $.Deferred();
var p2 = m.exec(function () { return d2; }).then(function (result) {
assert.strictEqual(result, 'd2');
});
assert.strictEqual(p2.state(), "pending");
d2.resolve('d2');
assert.strictEqual(p2.state(), "resolved");
});
QUnit.test('MutexedDropPrevious: 1 does not return a deferred', function (assert) {
assert.expect(5);
var m = new concurrency.MutexedDropPrevious();
var p1 = m.exec(function () { return 42; }).then(function (result) {
assert.strictEqual(result, 42);
});
assert.strictEqual(p1.state(), "resolved");
var d2 = $.Deferred();
var p2 = m.exec(function () { return d2; }).then(function (result) {
assert.strictEqual(result, 'd2');
});
assert.strictEqual(p2.state(), "pending");
d2.resolve('d2');
assert.strictEqual(p2.state(), "resolved");
});
QUnit.test('MutexedDropPrevious: 2 arrives before 1 resolution', function (assert) {
assert.expect(13);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var d2 = $.Deferred();
var p1 = m.exec(function () {
assert.step('p1');
return d1;
});
assert.strictEqual(p1.state(), "pending");
var p2 = m.exec(function () {
assert.step('p2');
return d2;
}).then(function (result) {
assert.strictEqual(result, 'd2');
});
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "pending");
assert.step('d1 resolved');
d1.resolve('d1');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "pending");
assert.step('d2 resolved');
d2.resolve('d2');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "resolved");
assert.verifySteps(['p1', 'd1 resolved', 'p2', 'd2 resolved']);
});
QUnit.test('MutexedDropPrevious: 3 arrives before 2 initialization', function (assert) {
assert.expect(13);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var d2 = $.Deferred();
var d3 = $.Deferred();
var p1 = m.exec(function () { return d1; });
assert.strictEqual(p1.state(), "pending");
var p2 = m.exec(function () {
assert.ok(false, "should not execute this function");
return d2;
});
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "pending");
var p3 = m.exec(function () { return d3; }).then(function (result) {
assert.strictEqual(result, 'd3');
});
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
assert.strictEqual(p3.state(), "pending");
d1.resolve('d1');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
assert.strictEqual(p3.state(), "pending");
d3.resolve('d3');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
assert.strictEqual(p3.state(), "resolved");
});
QUnit.test('MutexedDropPrevious: 3 arrives after 2 initialization', function (assert) {
assert.expect(19);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var d2 = $.Deferred();
var d3 = $.Deferred();
var p1 = m.exec(function () {
assert.step('p1');
return d1;
});
assert.strictEqual(p1.state(), "pending");
var p2 = m.exec(function () {
assert.step('p2');
return d2;
});
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "pending");
assert.step('d1 resolved');
d1.resolve('d1');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "pending");
var p3 = m.exec(function () {
assert.step('p3');
return d3;
}).then(function (result) {
assert.strictEqual(result, 'd3');
});
assert.step('d2 resolved');
d2.resolve('d2');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
assert.strictEqual(p3.state(), "pending");
assert.step('d3 resolved');
d3.resolve('d3');
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
assert.strictEqual(p3.state(), "resolved");
assert.verifySteps(['p1', 'd1 resolved', 'p2', 'd2 resolved', 'p3', 'd3 resolved']);
});
QUnit.test('MutexedDropPrevious: 2 in then of 1 with 3', function (assert) {
assert.expect(6);
var m = new concurrency.MutexedDropPrevious();
var d1 = $.Deferred();
var d2 = $.Deferred();
var d3 = $.Deferred();
var p3;
var p1 = m.exec(function () { return d1; })
.always(function () {
p3 = m.exec(function () {
return d3;
}).then(function (result) {
assert.strictEqual(result, 'd3');
});
return p3;
});
assert.strictEqual(p1.state(), "pending");
var p2 = m.exec(function () {
assert.ok(false, "should not execute this function");
return d2;
});
assert.strictEqual(p1.state(), "rejected");
assert.strictEqual(p2.state(), "rejected");
d1.resolve('d1');
assert.strictEqual(p3.state(), "pending");
d3.resolve('d3');
assert.strictEqual(p3.state(), "resolved");
});
});
});