actionhero
Version:
actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks
436 lines (382 loc) • 13.4 kB
JavaScript
var fs = require('fs');
var os = require('os');
var path = require('path');
var should = require('should');
var async = require('async');
var actionheroPrototype = require(__dirname + '/../../actionhero.js').actionheroPrototype;
var actionhero = new actionheroPrototype();
var api;
describe('Core: Cache', function(){
before(function(done){
actionhero.start(function(error, a){
api = a;
done();
});
});
after(function(done){
actionhero.stop(function(){
done();
});
});
it('cache methods should exist', function(done){
api.cache.should.be.an.instanceOf(Object);
api.cache.save.should.be.an.instanceOf(Function);
api.cache.load.should.be.an.instanceOf(Function);
api.cache.destroy.should.be.an.instanceOf(Function);
done();
});
it('cache.save', function(done){
api.cache.save('testKey', 'abc123', null, function(error, resp){
should.not.exist(error);
resp.should.equal(true);
done();
});
});
it('cache.load', function(done){
api.cache.load('testKey', function(error, resp){
resp.should.equal('abc123');
done();
});
});
it('cache.load failures', function(done){
api.cache.load('something else', function(error, resp){
String(error).should.equal('Error: Object not found');
should.equal(null, resp);
done();
});
});
it('cache.destroy', function(done){
api.cache.destroy('testKey', function(error, resp){
resp.should.equal(true);
done();
});
});
it('cache.destroy failure', function(done){
api.cache.destroy('testKey', function(error, resp){
resp.should.equal(false);
done();
});
});
it('cache.save with expire time', function(done){
api.cache.save('testKey', 'abc123', 10, function(error, resp){
resp.should.equal(true);
done();
});
});
it('cache.load with expired items should not return them', function(done){
api.cache.save('testKey_slow', 'abc123', 10, function(error, saveResp){
saveResp.should.equal(true);
setTimeout(function(){
api.cache.load('testKey_slow', function(error, loadResp){
String(error).should.equal('Error: Object Expired');
should.equal(null, loadResp);
done();
});
}, 20);
});
});
it('cache.load with negative expire times will never load', function(done){
api.cache.save('testKeyInThePast', 'abc123', -1, function(error, saveResp){
saveResp.should.equal(true);
api.cache.load('testKeyInThePast', function(error, loadResp){
(String(error).indexOf('Error: Object') >= 0).should.equal(true);
should.equal(null, loadResp);
done();
});
});
});
it('cache.save does not need to pass expireTime', function(done){
api.cache.save('testKeyForNullExpireTime', 'abc123', function(error, saveResp){
saveResp.should.equal(true);
api.cache.load('testKeyForNullExpireTime', function(error, loadResp){
loadResp.should.equal('abc123');
done();
});
});
});
it('cache.load without changing the expireTime will re-apply the redis expire', function(done){
var key = 'testKey';
api.cache.save(key, 'val', 1000, function(){
api.cache.load(key, function(error, loadResp){
loadResp.should.equal('val');
setTimeout(function(){
api.cache.load(key, function(error, loadResp){
String(error).should.equal('Error: Object not found');
should.equal(null, loadResp);
done();
});
}, 1001);
});
});
});
it('cache.load with options that extending expireTime should return cached item', function(done){
var expireTime = 400;
var timeout = 200;
//save the initial key
api.cache.save('testKey_slow', 'abc123', expireTime, function(error, saveResp){
saveResp.should.equal(true);
//wait for `timeout` and try to load the key
setTimeout(function(){
api.cache.load('testKey_slow', {expireTimeMS: expireTime}, function(error, loadResp){
loadResp.should.equal('abc123');
//wait another `timeout` and load the key again within the extended expire time
setTimeout(function(){
api.cache.load('testKey_slow', function(error, loadResp){
loadResp.should.equal('abc123');
//wait another `timeout` and the key load should fail without the extension
setTimeout(function(){
api.cache.load('testKey_slow', function(error, loadResp){
String(error).should.equal('Error: Object not found');
should.equal(null, loadResp);
done();
});
}, timeout);
});
}, timeout);
});
}, timeout);
});
});
it('cache.save works with arrays', function(done){
api.cache.save('array_key', [1, 2, 3], function(error, saveResp){
saveResp.should.equal(true);
api.cache.load('array_key', function(error, loadResp){
loadResp[0].should.equal(1);
loadResp[1].should.equal(2);
loadResp[2].should.equal(3);
done();
});
});
});
it('cache.save works with objects', function(done){
var data = {};
data.thing = 'stuff';
data.otherThing = [1, 2, 3];
api.cache.save('obj_key', data, function(error, saveResp){
saveResp.should.equal(true);
api.cache.load('obj_key', function(error, loadResp){
loadResp.thing.should.equal('stuff');
loadResp.otherThing[0].should.equal(1);
loadResp.otherThing[1].should.equal(2);
loadResp.otherThing[2].should.equal(3);
done();
});
});
});
it('can clear the cache entirely', function(done){
api.cache.save('thingA', 123, function(){
api.cache.size(function(error, count){
(count > 0).should.equal(true);
api.cache.clear(function(){
api.cache.size(function(error, count){
count.should.equal(0);
done();
});
});
});
});
});
describe('lists', function(){
it('can push and pop from an array', function(done){
var jobs = [];
jobs.push(function(next){ api.cache.push('testListKey', 'a string', next); });
jobs.push(function(next){ api.cache.push('testListKey', ['an array'], next); });
jobs.push(function(next){ api.cache.push('testListKey', {what: 'an aobject'}, next); });
async.parallel(jobs, function(error){
should.not.exist(error);
jobs = [];
jobs.push(function(next){
api.cache.pop('testListKey', function(error, data){
data.should.equal('a string');
next();
});
});
jobs.push(function(next){
api.cache.pop('testListKey', function(error, data){
data.should.deepEqual(['an array']);
next();
});
});
jobs.push(function(next){
api.cache.pop('testListKey', function(error, data){
data.should.deepEqual({what: 'an aobject'});
next();
});
});
async.series(jobs, function(error){
should.not.exist(error);
done();
});
});
});
it('will return null if the list is empty', function(done){
api.cache.pop('emptyListKey', function(error, data){
should.not.exist(error);
should.not.exist(data);
done();
});
});
it('can get the length of an array when full', function(done){
api.cache.push('testListKey2', 'a string', function(){
api.cache.listLength('testListKey2', function(error, l){
should.not.exist(error);
l.should.equal(1);
done();
});
});
});
it('will return 0 length when the key does not exist', function(done){
api.cache.listLength('testListKey3', function(error, l){
should.not.exist(error);
l.should.equal(0);
done();
});
});
});
describe('locks', function(){
var key = 'testKey';
afterEach(function(done){
api.cache.lockName = api.id;
api.cache.unlock(key, function(){
done();
});
});
it('things can be locked, checked, and unlocked aribitrarily', function(done){
api.cache.lock(key, 100, function(error, lockOk){
lockOk.should.equal(true);
api.cache.checkLock(key, null, function(error, lockOk){
lockOk.should.equal(true);
api.cache.unlock(key, function(error, lockOk){
lockOk.should.equal(true);
done();
});
});
});
});
it('locks have a TTL and the default will be assumed from config', function(done){
api.cache.lock(key, null, function(error, lockOk){
lockOk.should.equal(true);
api.redis.client.ttl(api.cache.lockPrefix + key, function(error, ttl){
(ttl >= 9).should.equal(true);
(ttl <= 10).should.equal(true);
done();
});
});
});
it('you can save an item if you do hold the lock', function(done){
api.cache.lock(key, null, function(error, lockOk){
lockOk.should.equal(true);
api.cache.save(key, 'value', function(error, success){
success.should.equal(true);
done();
});
});
});
it('you cannot save a locked item if you do not hold the lock', function(done){
api.cache.lock(key, null, function(error, lockOk){
lockOk.should.equal(true);
api.cache.lockName = 'otherId';
api.cache.save(key, 'value', function(error){
String(error).should.equal('Error: Object Locked');
done();
});
});
});
it('you cannot destroy a locked item if you do not hold the lock', function(done){
api.cache.lock(key, null, function(error, lockOk){
lockOk.should.equal(true);
api.cache.lockName = 'otherId';
api.cache.destroy(key, function(error){
String(error).should.equal('Error: Object Locked');
done();
});
});
});
it('you can opt to retry to obtain a lock if a lock is held (READ)', function(done){
api.cache.lock(key, 1, function(error, lockOk){ // will be rounded up to 1s
lockOk.should.equal(true);
api.cache.save(key, 'value', function(error, success){
success.should.equal(true);
api.cache.lockName = 'otherId';
api.cache.checkLock(key, null, function(error, lockOk){
lockOk.should.equal(false);
var start = new Date().getTime();
api.cache.load(key, {retry: 2000}, function(error, data){
data.should.equal('value');
var delta = new Date().getTime() - start;
(delta >= 1000).should.equal(true);
done();
});
});
});
});
});
describe('locks are actually blocking', function(){
var originalLockName;
before(function(){
originalLockName = api.cache.lockName;
});
after(function(){
api.cache.lockName = originalLockName;
});
it('locks are actually blocking', function(done){
var key = 'test';
var locksRetrieved = 0;
var locksRejected = 0;
var concurentLocksCount = 100;
var jobs = [];
var go = function(next){
// proxy for another actionhero instance accessing the same locked object
api.cache.lockName = 'test-name-pass-' + (locksRetrieved + locksRejected);
api.cache.checkLock(key, null, function(error, lockOk){
if(error){ return next(error); }
if(lockOk){
locksRetrieved++;
api.cache.lock(key, (1000 * 60), next);
}else{
locksRejected++;
next();
}
});
};
for(var i = 0; i < concurentLocksCount; i++){
jobs.push(go);
}
async.series(jobs, function(error){
should.not.exist(error);
locksRetrieved.should.be.equal(1); // Only first atempt
locksRejected.should.be.equal(concurentLocksCount - 1); // Everything else
done();
});
});
});
});
describe('cache dump files', function(){
if(typeof os.tmpdir !== 'function'){ os.tmpdir = os.tmpDir; } // resolution for node v0.8.x
var file = os.tmpdir() + path.sep + 'cacheDump';
it('can read write the cache to a dump file', function(done){
api.cache.clear(function(){
api.cache.save('thingA', 123, function(){
api.cache.dumpWrite(file, function(error, count){
count.should.equal(1);
var body = JSON.parse(String(fs.readFileSync(file)));
var content = JSON.parse(body['actionhero:cache:thingA']);
content.value.should.equal(123);
done();
});
});
});
});
it('can laod the cache from a dump file', function(done){
api.cache.clear(function(){
api.cache.dumpRead(file, function(error, count){
count.should.equal(1);
api.cache.load('thingA', function(error, value){
value.should.equal(123);
done();
});
});
});
});
});
});