Commit 428d97dd authored by Romain Courteaud's avatar Romain Courteaud 🐸

Add mutex implementation.

Mutex execution should be cancellable.
callback should always be cancellable without preventing the mutex to be released.
parent f7281a1e
......@@ -75,7 +75,7 @@ module.exports = function (grunt) {
test: {
src: ['test/embedded.js', 'test/renderjs_test.js',
'test/embedded.js', 'test/inject_script.js',
'test/not_declared_gadget.js',
'test/mutex_test.js', 'test/not_declared_gadget.js',
'test/trigger_rjsready_event_on_ready_gadget.js'],
directives: {
maxlen: 79,
......
......@@ -142,6 +142,7 @@
gadget_loading_klass_list = [],
renderJS,
Monitor,
Mutex,
scope_increment = 0,
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'),
is_page_unloaded = false,
......@@ -158,6 +159,64 @@
is_page_unloaded = true;
});
/////////////////////////////////////////////////////////////////
// Mutex
/////////////////////////////////////////////////////////////////
Mutex = function createMutex() {
if (!(this instanceof Mutex)) {
return new Mutex();
}
this._latest_defer = null;
};
Mutex.prototype = {
constructor: Mutex,
lock: function lockMutex() {
var previous_defer = this._latest_defer,
current_defer = RSVP.defer(),
queue = new RSVP.Queue();
this._latest_defer = current_defer;
if (previous_defer !== null) {
queue.push(function acquireMutex() {
return previous_defer.promise;
});
}
// Create a new promise (.then) not cancellable
// to allow external cancellation of the callback
// without breaking the mutex implementation
queue
.fail(current_defer.resolve.bind(current_defer));
return queue
.push(function generateMutexUnlock() {
return function runAndUnlock(callback) {
return new RSVP.Queue()
.push(function executeMutexCallback() {
return callback();
})
.push(function releaseMutexAfterSuccess(result) {
current_defer.resolve(result);
return result;
}, function releaseMutexAfterError(error) {
current_defer.resolve(error);
throw error;
});
};
});
},
lockAndRun: function (callback) {
return this.lock()
.push(function executeLockAndRunCallback(runAndUnlock) {
return runAndUnlock(callback);
});
}
};
/////////////////////////////////////////////////////////////////
// Helper functions
/////////////////////////////////////////////////////////////////
......@@ -1371,6 +1430,7 @@
/////////////////////////////////////////////////////////////////
// global
/////////////////////////////////////////////////////////////////
renderJS.Mutex = Mutex;
window.rJS = window.renderJS = renderJS;
window.__RenderJSGadget = RenderJSGadget;
window.__RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
......
......@@ -11,6 +11,7 @@
<script src="../node_modules/URIjs/src/URI.js"></script>
<script src="../dist/renderjs-latest.js" type="text/javascript"></script>
<script src="renderjs_test.js" type="text/javascript"></script>
<script src="mutex_test.js" type="text/javascript"></script>
</head>
<body>
<h1 id="qunit-header">QUnit renderJS test suite</h1>
......
(function (Mutex, QUnit) {
"use strict";
var test = QUnit.test,
stop = QUnit.stop,
start = QUnit.start,
ok = QUnit.ok,
expect = QUnit.expect,
equal = QUnit.equal,
module = QUnit.module;
/////////////////////////////////////////////////////////////////
// parseGadgetHTMLDocument
/////////////////////////////////////////////////////////////////
module("renderJS.Mutex");
test('constructor', function () {
equal(Mutex.length, 0);
var mutex = new Mutex();
equal(Object.getPrototypeOf(mutex), Mutex.prototype);
equal(mutex.constructor, Mutex);
equal(Mutex.prototype.constructor, Mutex);
});
test('lockAndRun execute callback', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(6);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
return new RSVP.Queue()
.push(function () {
return RSVP.delay(100);
})
.push(function () {
assertCounter(1);
return 'callback1 result';
});
}
function callback2() {
assertCounter(3);
}
return new RSVP.Queue()
.push(function () {
return mutex.lockAndRun(callback1);
})
.push(function (result) {
equal(result, 'callback1 result');
assertCounter(2);
return mutex.lockAndRun(callback2);
})
.push(function () {
assertCounter(4);
})
.always(function () {
start();
});
});
test('lockAndRun handle exception', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(5);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
throw new Error('Error in callback1');
}
function callback2() {
assertCounter(2);
}
return new RSVP.Queue()
.push(function () {
return mutex.lockAndRun(callback1);
})
.push(undefined, function (error) {
equal(error.message, 'Error in callback1');
assertCounter(1);
return mutex.lockAndRun(callback2);
})
.push(function () {
assertCounter(3);
})
.always(function () {
start();
});
});
test('lockAndRun prevent concurrent execution', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(9);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
return new RSVP.Queue()
.push(function () {
return RSVP.delay(50);
})
.push(function () {
assertCounter(1);
return 'callback1 result';
});
}
function callback2() {
assertCounter(2);
return new RSVP.Queue()
.push(function () {
return RSVP.delay(50);
})
.push(function () {
assertCounter(3);
return 'callback2 result';
});
}
function callback3() {
assertCounter(4);
return 'callback3 result';
}
return new RSVP.Queue()
.push(function () {
return RSVP.all([
mutex.lockAndRun(callback1),
mutex.lockAndRun(callback2),
mutex.lockAndRun(callback3)
]);
})
.push(function (result_list) {
equal(result_list[0], 'callback1 result');
equal(result_list[1], 'callback2 result');
equal(result_list[2], 'callback3 result');
assertCounter(5);
})
.always(function () {
start();
});
});
test('lockAndRun handle concurrent exception', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(4);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
throw new Error('error in callback1');
}
function callback2() {
assertCounter(1);
throw new Error('error in callback2');
}
function callback3() {
assertCounter(2);
return 'callback3 result';
}
return new RSVP.Queue()
.push(function () {
return RSVP.all([
mutex.lockAndRun(callback1),
mutex.lockAndRun(callback2),
mutex.lockAndRun(callback3)
]);
})
.push(undefined, function (error) {
equal(error.message, 'error in callback1');
assertCounter(2);
})
.always(function () {
start();
});
});
test('lockAndRun cancel does not prevent next execution', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(6);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
ok(false, 'Should not reach that code');
}
function callback2() {
assertCounter(1);
return 'callback2 result';
}
return new RSVP.Queue()
.push(function () {
var promise1 = mutex.lockAndRun(callback1);
return RSVP.all([
promise1
.then(function () {
ok(false, 'Should not reach that code');
}, function (error) {
assertCounter(0);
equal(error.message, 'Default Message');
return 'handler1 result';
}),
mutex.lockAndRun(callback2),
promise1.cancel('cancel callback1')
]);
})
.push(function (result_list) {
equal(result_list[0], 'handler1 result');
equal(result_list[1], 'callback2 result');
assertCounter(2);
})
.always(function () {
start();
});
});
test('lockAndRun cancel does not cancel previous execution', function () {
var mutex = new Mutex(),
counter = 0,
defer = RSVP.defer();
stop();
expect(8);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback2() {
ok(false, 'Should not reach that code');
}
function callback1() {
return new RSVP.Queue()
.push(function () {
return RSVP.delay(50);
})
.push(function () {
assertCounter(0);
defer.resolve();
return RSVP.delay(50);
})
.push(function () {
assertCounter(3);
return 'callback1 result';
});
}
return new RSVP.Queue()
.push(function () {
var promise1 = mutex.lockAndRun(callback1),
promise2 = mutex.lockAndRun(callback2);
return RSVP.all([
promise1,
promise2
.then(function () {
ok(false, 'Should not reach that code');
}, function (error) {
assertCounter(2);
equal(error.message, 'Default Message');
return 'handler2 result';
}),
defer.promise
.then(function () {
assertCounter(1);
promise2.cancel('cancel callback2');
})
]);
})
.push(function (result_list) {
equal(result_list[0], 'callback1 result');
equal(result_list[1], 'handler2 result');
assertCounter(4);
})
.always(function () {
start();
});
});
test('lockAndRun only wait for the callback', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(4);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
return 'callback1 result';
}
function callback2() {
assertCounter(1);
return 'callback2 result';
}
return new RSVP.Queue()
.push(function () {
return RSVP.any([
mutex.lockAndRun(callback1)
.push(function () {
return RSVP.delay(10000);
}),
mutex.lockAndRun(callback2)
]);
})
.push(function (result) {
equal(result, 'callback2 result');
assertCounter(2);
})
.always(function () {
start();
});
});
test('lockAndRun only wait for the error callback', function () {
var mutex = new Mutex(),
counter = 0;
stop();
expect(4);
function assertCounter(value) {
equal(counter, value);
counter += 1;
}
function callback1() {
assertCounter(0);
throw new Error('callback1 error');
}
function callback2() {
assertCounter(1);
return 'callback2 result';
}
return new RSVP.Queue()
.push(function () {
return RSVP.any([
mutex.lockAndRun(callback1)
.push(undefined, function () {
return RSVP.delay(10000);
}),
mutex.lockAndRun(callback2)
]);
})
.push(function (result) {
equal(result, 'callback2 result');
assertCounter(2);
})
.always(function () {
start();
});
});
}(renderJS.Mutex, QUnit));
\ No newline at end of file
......@@ -5891,6 +5891,8 @@
URI("../dist/renderjs-latest.js")
.absoluteTo(parent_path).toString(),
URI("renderjs_test.js")
.absoluteTo(parent_path).toString(),
URI("mutex_test.js")
.absoluteTo(parent_path).toString()
]);
equal(root_gadget.element.outerHTML, document.body.outerHTML);
......@@ -5914,6 +5916,8 @@
URI("../dist/renderjs-latest.js")
.absoluteTo(parent_path).toString(),
URI("renderjs_test.js")
.absoluteTo(parent_path).toString(),
URI("mutex_test.js")
.absoluteTo(parent_path).toString()
]);
html = root_gadget.constructor.__template_element.outerHTML;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment