ember-app-scheduler
Version:
Ember addon to schedule work at different phases of app life cycle.
593 lines (448 loc) • 15.4 kB
JavaScript
var assert = require('assert'),
Promise = require('../lib/Promise'),
Pool = require('../lib/Pool');
function add(a, b) {
return a + b;
}
describe('Pool', function () {
it('should offload a function to a worker', function (done) {
var pool = new Pool({maxWorkers: 10});
function add(a, b) {
return a + b;
}
assert.equal(pool.workers.length, 0);
pool.exec(add, [3, 4])
.then(function (result) {
assert.equal(result, 7);
assert.equal(pool.workers.length, 1);
pool.clear();
assert.equal(pool.workers.length, 0);
done();
});
assert.equal(pool.workers.length, 1);
});
it('should offload functions to multiple workers', function (done) {
var pool = new Pool({maxWorkers: 10});
function add(a, b) {
return a + b;
}
assert.equal(pool.workers.length, 0);
Promise.all([
pool.exec(add, [3, 4]),
pool.exec(add, [2, 3])
])
.then(function (results) {
assert.deepEqual(results, [7, 5]);
assert.equal(pool.workers.length, 2);
pool.clear();
done();
});
assert.equal(pool.workers.length, 2);
});
it('should put tasks in queue when all workers are busy', function (done) {
var pool = new Pool({maxWorkers: 2});
function add(a, b) {
return a + b;
}
assert.equal(pool.tasks.length, 0);
assert.equal(pool.workers.length, 0);
var task1 = pool.exec(add, [3, 4]);
var task2 = pool.exec(add, [2, 3]);
assert.equal(pool.tasks.length, 0);
assert.equal(pool.workers.length, 2);
var task3 = pool.exec(add, [5, 7]);
var task4 = pool.exec(add, [1, 1]);
assert.equal(pool.tasks.length, 2);
assert.equal(pool.workers.length, 2);
Promise.all([
task1,
task2,
task3,
task4
])
.then(function (results) {
assert.deepEqual(results, [7, 5, 12, 2]);
assert.equal(pool.tasks.length, 0);
assert.equal(pool.workers.length, 2);
pool.clear();
done();
});
});
it('should create a proxy', function (done) {
var pool = new Pool();
pool.proxy().then(function (proxy) {
assert.deepEqual(Object.keys(proxy).sort(), ['methods', 'run']);
proxy.methods()
.then(function (methods) {
assert.deepEqual(methods.sort(), ['methods', 'run']);
done();
})
.catch(function (err) {
assert('Should not throw an error');
});
});
});
it('should create a proxy of a custom worker', function (done) {
var pool = new Pool(__dirname + '/workers/simple.js');
pool.proxy().then(function (proxy) {
assert.deepEqual(Object.keys(proxy).sort(), ['add','methods','multiply','run','timeout']);
done();
});
});
it('should invoke a method via a proxy', function (done) {
var pool = new Pool(__dirname + '/workers/simple.js');
pool.proxy().then(function (proxy) {
proxy.multiply(4, 3)
.then(function (result) {
assert.equal(result, 12);
done();
})
.catch(function (err) {
console.log(err);
assert('Should not throw an error');
});
});
});
it('should invoke an async method via a proxy', function (done) {
var pool = new Pool(__dirname + '/workers/simple.js');
pool.proxy().then(function (proxy) {
proxy.timeout(100)
.then(function (result) {
assert.equal(result, 'done');
done();
})
.catch(function (err) {
console.log(err);
assert('Should not throw an error');
});
});
});
it('should handle errors thrown by a worker', function (done) {
var pool = new Pool({maxWorkers: 10});
function test() {
throw new TypeError('Test error');
}
pool.exec(test)
.catch(function (err) {
assert.ok(err instanceof Error);
assert.equal(err.message, 'Test error')
pool.clear();
done();
});
});
it('should execute a function returning a Promise', function (done) {
var pool = new Pool({maxWorkers: 10});
function testAsync() {
return Promise.resolve('done');
}
pool.exec(testAsync)
.then(function (result) {
assert.equal(result, 'done');
done();
})
.catch(function (err) {
assert('Should not throw an error');
});
});
it('should propagate a rejected Promise', function (done) {
var pool = new Pool({maxWorkers: 10});
function testAsync() {
return Promise.reject(new Error('I reject!'));
}
pool.exec(testAsync)
.then(function (result) {
assert('Should not resolve');
})
.catch(function (err) {
//assert.ok(err instanceof Error); // FIXME: returned error should be an instanceof Error
assert.equal(err, err.toString('Error: I reject!'));
done();
});
});
it('should cancel a task', function (done) {
var pool = new Pool({maxWorkers: 10});
function forever() {
while (1 > 0) {} // runs forever
}
var promise = pool.exec(forever)
.then(function (result) {
assert('promise should never resolve');
})
//.catch(Promise.CancellationError, function (err) { // TODO: not yet supported
.catch(function (err) {
assert(err instanceof Promise.CancellationError);
assert.equal(pool.workers.length, 0);
done();
});
// cancel the task
setTimeout(function () {
promise.cancel();
}, 0);
});
it('should cancel a queued task', function (done) {
var pool = new Pool({maxWorkers: 1});
var reachedTheEnd = false;
function delayed() {
var Promise = require('../lib/Promise');
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1);
}, 0);
});
}
function one() {
return 1;
}
var p1 = pool.exec(delayed)
.then(function (result) {
assert.equal(result, 1);
assert.equal(reachedTheEnd, true);
assert.equal(pool.workers.length, 1);
assert.equal(pool.tasks.length, 0);
done();
});
assert.equal(pool.workers.length, 1);
assert.equal(pool.tasks.length, 0);
var p2 = pool.exec(one); // will be queued
assert.equal(pool.workers.length, 1);
assert.equal(pool.tasks.length, 1);
p2.cancel(); // cancel immediately
assert.equal(pool.workers.length, 1);
assert.equal(pool.tasks.length, 1);
reachedTheEnd = true;
});
// TODO: test whether a task in the queue can be neatly cancelled
it('should timeout a task', function (done) {
var pool = new Pool({maxWorkers: 10});
function forever() {
while (1 > 0) {} // runs forever
}
var promise = pool.exec(forever)
.timeout(50)
.then(function (result) {
assert('promise should never resolve');
})
//.catch(Promise.CancellationError, function (err) { // TODO: not yet supported
.catch(function (err) {
assert(err instanceof Promise.TimeoutError);
assert.equal(pool.workers.length, 0);
done();
});
});
it('should start timeout timer of a task once the task is taken from the queue (1)', function (done) {
var pool = new Pool({maxWorkers: 1});
var delay = 50
function sleep() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve ('done :)')
}, 100) // 2 * delay
})
}
function doNothing() {
return 'ready'
}
// add a task
pool.exec(sleep)
// add a second task, will be queued until the first finishes
// the timeout is shorter than the currently executing task and longer than
// the queued task, so it should not timeout
pool.exec(doNothing)
.timeout(delay)
.then(function (result) {
assert.equal(result, 'ready');
done();
})
.catch(function (err) {
assert('promise should not throw');
});
});
it('should start timeout timer of a task once the task is taken from the queue (2)', function (done) {
var pool = new Pool({maxWorkers: 1});
var delay = 50
function sleep() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve ('done :)')
}, 100) // 2 * delay
})
}
// add a task
pool.exec(sleep)
// add a second task, will be queued until the first finishes
pool.exec(sleep)
.timeout(delay)
.then(function (result) {
assert('promise should never resolve');
})
.catch(function (err) {
assert(err instanceof Promise.TimeoutError);
done();
});
});
it('should handle crashed workers (1)', function (done) {
var pool = new Pool({maxWorkers: 1});
pool.exec(add)
.then(function () {
assert('Promise should not be resolved');
})
.catch(function (err) {
assert.equal(err.toString(), 'Error: Worker terminated unexpectedly');
assert.equal(pool.workers.length, 0);
// validate whether a new worker is spawned
pool.exec(add, [2,3])
.then(function (result) {
assert.equal(result, 5);
assert.equal(pool.workers.length, 1);
pool.clear();
done();
});
assert.equal(pool.workers.length, 1);
});
assert.equal(pool.workers.length, 1);
// kill the worker so it will be terminated
pool.workers[0].worker.kill();
assert.equal(pool.workers.length, 1);
});
describe('options', function () {
it('should throw an error on invalid type or number of maxWorkers', function () {
assert.throws(function () {
new Pool({maxWorkers: 'a string'});
}, TypeError);
assert.throws(function () {
new Pool({maxWorkers: 2.5});
}, TypeError);
assert.throws(function () {
new Pool({maxWorkers: 0});
}, TypeError);
assert.throws(function () {
new Pool({maxWorkers: -1});
}, TypeError);
});
it('should limit to the configured number of max workers', function () {
var pool = new Pool({maxWorkers: 2});
pool.exec(add, [1, 2]);
pool.exec(add, [3, 4]);
pool.exec(add, [5, 6]);
pool.exec(add, [7, 8]);
pool.exec(add, [9, 0]);
assert.equal(pool.maxWorkers, 2);
assert.equal(pool.workers.length, 2);
assert.equal(pool.tasks.length, 3);
pool.clear();
});
it('should take number of cpus minus one as default maxWorkers', function () {
var pool = new Pool();
var cpus = require('os').cpus();
assert.equal(pool.maxWorkers, cpus.length - 1);
pool.clear();
});
it('should throw an error on invalid type or number of minWorkers', function () {
assert.throws(function () {
new Pool({minWorkers: 'a string'});
}, TypeError);
assert.throws(function () {
new Pool({minWorkers: 2.5});
}, TypeError);
assert.throws(function () {
new Pool({maxWorkers: -1});
}, TypeError);
});
it('should create number of cpus minus one when minWorkers set to \'max\'', function () {
var pool = new Pool({minWorkers:'max'});
var cpus = require('os').cpus();
assert.equal(pool.workers.length, cpus.length - 1);
pool.clear();
});
it('should increase maxWorkers to match minWorkers', function () {
var pool = new Pool({minWorkers: 16});
for(var i=0;i<20;i++) pool.exec(add, [i, i*2]);
assert.equal(pool.minWorkers, 16);
assert.equal(pool.maxWorkers, 16);
assert.equal(pool.workers.length, 16);
assert.equal(pool.tasks.length, 4);
pool.clear();
});
});
it.skip('should handle crashed workers (2)', function (done) {
// TODO: create a worker from a script, which really crashes itself
});
it('should clear all workers', function (done) {
var pool = new Pool({maxWorkers: 10});
assert.equal(pool.workers.length, 0);
function test() {
return 'ok';
}
pool.exec(test)
.then(function (result) {
assert.equal(result, 'ok');
assert.equal(pool.workers.length, 1);
pool.clear();
assert.equal(pool.workers.length, 0);
done();
});
assert.equal(pool.workers.length, 1);
});
it('should clear all workers after tasks are finished', function (done) {
var pool = new Pool({maxWorkers: 10});
assert.equal(pool.workers.length, 0);
function test() {
return 'ok';
}
pool.exec(test)
.then(function (result) {
assert.equal(result, 'ok');
assert.equal(pool.workers.length, 0);
done();
});
assert.equal(pool.workers.length, 1);
pool.clear();
assert.equal(pool.workers.length, 0);
});
it('should return statistics', function () {
var pool = new Pool({maxWorkers: 4});
function test() {
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
});
}
function testError() {
return new Promise(function (resolve, reject) {
throw new Error('Test error')
});
}
assert.deepEqual(pool.stats(), {totalWorkers: 0, busyWorkers: 0, idleWorkers: 0, pendingTasks: 0, activeTasks: 0});
var promise = pool.exec(test)
.then(function () {
assert.deepEqual(pool.stats(), {totalWorkers: 1, busyWorkers: 0, idleWorkers: 1, pendingTasks: 0, activeTasks: 0 });
// start six tasks (max workers is 4, so we should get pending tasks)
var all = Promise.all([
pool.exec(test),
pool.exec(test),
pool.exec(test),
pool.exec(test),
pool.exec(test),
pool.exec(test)
]);
assert.deepEqual(pool.stats(), {totalWorkers: 4, busyWorkers: 4, idleWorkers: 0, pendingTasks: 2, activeTasks: 4});
return all;
})
.then(function () {
assert.deepEqual(pool.stats(), {totalWorkers: 4, busyWorkers: 0, idleWorkers: 4, pendingTasks: 0, activeTasks: 0 });
return pool.exec(testError)
})
.catch(function () {
assert.deepEqual(pool.stats(), {totalWorkers: 4, busyWorkers: 0, idleWorkers: 4, pendingTasks: 0, activeTasks: 0});
});
assert.deepEqual(pool.stats(), {totalWorkers: 1, busyWorkers: 1, idleWorkers: 0, pendingTasks: 0, activeTasks: 1});
return promise;
});
it('should throw an error in case of wrong type of arguments in function exec', function () {
var pool = new Pool();
assert.throws(function () {pool.exec()}, TypeError);
assert.throws(function () {pool.exec(23)}, TypeError);
assert.throws(function () {pool.exec(add, {})}, TypeError);
assert.throws(function () {pool.exec(add, 2, 3)}, TypeError);
assert.throws(function () {pool.exec(add, 'a string')}, TypeError);
});
});