node-stash
Version:
Distributed cache using Redis and in-memory LRU
292 lines (231 loc) • 6.91 kB
JavaScript
var assert = require('assert');
var Stash = require('../lib/stash');
var Warlock = require('node-redis-warlock');
var async = require('neo-async');
var Redis = require('redis');
var createStash = Stash.createStash;
var mockDbFetch = function(cb) {
setImmediate(function() {
cb(null, { test: 1 });
});
};
var dbErrMsg = 'DB AINT COOL RIGHT NOW';
var mockDbFetchErr = function(cb) {
setImmediate(function() {
cb(new Error(dbErrMsg));
});
};
var createRedisClient = function() {
return Redis.createClient();
};
describe('Stash', function() {
var stash = createStash(createRedisClient);
var redis = stash.cacheRedis;
describe('get', function() {
it('errors if no fetch function given', function(done) {
stash.get('test', null, function(err){
assert(err);
done();
});
});
it('no-ops if no cb given', function() {
stash.get('test');
});
it('fetches uncached data', function(done) {
stash.get('test', mockDbFetch, function(err, data){
assert(!err);
assert.equal(1, data.test);
done();
});
});
it('saved to cache', function(done) {
redis.get('test', function(err, data) {
assert(!err);
assert(data);
data = stash.decodeContent(data)[0];
assert.equal(1, data.test);
done();
});
});
it('fetches cached data', function(done) {
stash.get('test', function(){
throw new Error('Fetcher should not be invoked');
}, function(err, data){
assert(!err);
assert(data);
assert.equal(1, data.test);
done();
});
});
});
describe('delete', function() {
before(function(done) {
redis.exists('test', function(err, exists) {
assert(1, exists);
stash.del('test', done);
});
});
it('should remove from redis', function(done) {
redis.exists('test', function(err, exists) {
assert(true, exists);
done(err);
});
});
it('should clear', function() {
stash.clear();
});
});
describe('during cache issues', function(done) {
var stash = createStash(createRedisClient, {
redis: {
wait: false
}
});
before(function(done) {
stash.cacheRedis.end();
done();
});
it('gives error when getting a key', function(done) {
stash.get('blah', mockDbFetch, function(err, data){
assert(err);
assert.equal('redis unavailable', err.message);
done();
});
});
it('gives error when deleting a key', function(done) {
stash.del('blah', function(err, data){
assert(err);
assert.equal('redis unavailable', err.message);
done();
});
});
});
describe('during db issues', function(done) {
var stash = createStash(createRedisClient);
it('db error is cached', function(done) {
var dbFetches = 0;
var fetch = function(cb) {
assert.equal(1, (++dbFetches));
return mockDbFetchErr(cb);
};
stash.get('blah1', fetch, function(err1) {
stash.get('blah1', fetch, function(err2, data){
assert(!data);
assert.equal(dbErrMsg, err2.message);
assert.equal(err2, err1);
done();
});
});
});
});
after(cb => stash.del('blah1', cb));
describe('concurrency', function() {
var stash = createStash(createRedisClient);
var redis = stash.cacheRedis;
var warlock = Warlock(redis);
it('sets lock when db fetching', function(done){
stash.get('sheep', function(cb){
warlock.lock('sheep', stash.conf.redis.ttl.lock, function(err, unlock){
assert(!err);
assert.equal(false, !!unlock);
cb();
done();
});
}, function() {});
});
after(cb => stash.del('sheep', cb));
it('stops retrying if locked and retry limit reached', function(done) {
var stash2 = createStash(createRedisClient, {
retryLimit: 0,
timeout: {
retry: 1
}
});
stash.get('retryLimit', function(cb) { }, function(){});
stash2.get('retryLimit', function(cb){ }, function(err){
assert.equal('retry limit reached', err.message);
done();
});
});
after(cb => stash.del('retryLimit', cb));
it('many gets on uncached key from a single instance result in only one db fetch', function(done) {
var numGets = 100;
var fetches = 0;
var doGet = function(n, next) {
stash.get('hotKey', function(cb) {
assert.equal(1, (++fetches));
return setImmediate(function() {
cb(null, { test: 2 });
});
}, next);
};
async.times(numGets, doGet, function(err, results) {
assert(!err);
assert.equal(numGets, results.length);
done();
});
});
after(cb => stash.del('hotKey', cb));
it('gets from multiple separate instances should only fetch from db once', function(done) {
var instances = [];
var numInstances = 5;
var fetches = 0;
for (var i = 0; i < numInstances; i++) {
instances.push(createStash(createRedisClient, {
timeout: {
retry: 1
}
}));
}
async.map(instances, function(stash, done) {
stash.get('multiInstance', function(cb) {
assert.equal(1, (++fetches));
return setImmediate(function() {
cb(null, { test: 2 });
});
}, done);
}, done);
});
});
after(cb => stash.del('multiInstance', cb));
describe('broadcast', function() {
var stash = createStash(createRedisClient);
var redis = stash.cacheRedis;
var stash2 = createStash(createRedisClient);
before(function(done) {
// precache
stash.get('pizza', mockDbFetch, function(err, data){
assert(!err);
assert(data);
assert.equal(1, data.test);
// Make sure cache key is actually there
redis.get('pizza', function(err, data){
assert(!err);
assert(data);
stash2.get('pizza', mockDbFetch, function(err, data) {
assert.equal(1, data.test);
return done(err);
});
});
});
});
it('should invalidate local cache for all instances', function(done) {
var numFetches = 0;
var fetch = function(cb) {
numFetches += 1;
setImmediate(cb);
};
stash2.broadcast.once('message', function(){
var purgeTest = function() {
stash2.get('pizza', fetch, function() {
assert.equal(1, numFetches);
return done();
});
};
setImmediate(purgeTest);
});
stash.invalidate('pizza');
});
after(cb => stash.del('pizza', cb));
});
});