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) { ...@@ -75,7 +75,7 @@ module.exports = function (grunt) {
test: { test: {
src: ['test/embedded.js', 'test/renderjs_test.js', src: ['test/embedded.js', 'test/renderjs_test.js',
'test/embedded.js', 'test/inject_script.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'], 'test/trigger_rjsready_event_on_ready_gadget.js'],
directives: { directives: {
maxlen: 79, maxlen: 79,
......
...@@ -142,6 +142,7 @@ ...@@ -142,6 +142,7 @@
gadget_loading_klass_list = [], gadget_loading_klass_list = [],
renderJS, renderJS,
Monitor, Monitor,
Mutex,
scope_increment = 0, scope_increment = 0,
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'), isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'),
is_page_unloaded = false, is_page_unloaded = false,
...@@ -158,6 +159,64 @@ ...@@ -158,6 +159,64 @@
is_page_unloaded = true; 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 // Helper functions
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -1371,6 +1430,7 @@ ...@@ -1371,6 +1430,7 @@
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// global // global
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
renderJS.Mutex = Mutex;
window.rJS = window.renderJS = renderJS; window.rJS = window.renderJS = renderJS;
window.__RenderJSGadget = RenderJSGadget; window.__RenderJSGadget = RenderJSGadget;
window.__RenderJSEmbeddedGadget = RenderJSEmbeddedGadget; window.__RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
<script src="../node_modules/URIjs/src/URI.js"></script> <script src="../node_modules/URIjs/src/URI.js"></script>
<script src="../dist/renderjs-latest.js" type="text/javascript"></script> <script src="../dist/renderjs-latest.js" type="text/javascript"></script>
<script src="renderjs_test.js" type="text/javascript"></script> <script src="renderjs_test.js" type="text/javascript"></script>
<script src="mutex_test.js" type="text/javascript"></script>
</head> </head>
<body> <body>
<h1 id="qunit-header">QUnit renderJS test suite</h1> <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 @@ ...@@ -5891,6 +5891,8 @@
URI("../dist/renderjs-latest.js") URI("../dist/renderjs-latest.js")
.absoluteTo(parent_path).toString(), .absoluteTo(parent_path).toString(),
URI("renderjs_test.js") URI("renderjs_test.js")
.absoluteTo(parent_path).toString(),
URI("mutex_test.js")
.absoluteTo(parent_path).toString() .absoluteTo(parent_path).toString()
]); ]);
equal(root_gadget.element.outerHTML, document.body.outerHTML); equal(root_gadget.element.outerHTML, document.body.outerHTML);
...@@ -5914,6 +5916,8 @@ ...@@ -5914,6 +5916,8 @@
URI("../dist/renderjs-latest.js") URI("../dist/renderjs-latest.js")
.absoluteTo(parent_path).toString(), .absoluteTo(parent_path).toString(),
URI("renderjs_test.js") URI("renderjs_test.js")
.absoluteTo(parent_path).toString(),
URI("mutex_test.js")
.absoluteTo(parent_path).toString() .absoluteTo(parent_path).toString()
]); ]);
html = root_gadget.constructor.__template_element.outerHTML; 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