[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:
committed by
Christophe Matthieu
parent
971629b9fe
commit
7f4b4f86bd
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user