Browse Source

initial implementation

hotfix/0.7.1
Bryan Donovan 12 years ago
parent
commit
eeb9f7f751
  1. 1
      .gitignore
  2. 6
      index.js
  3. 47
      lib/caching.js
  4. 79
      lib/multi_caching.js
  5. 38
      lib/stores/memory.js
  6. 68
      lib/stores/redis.js
  7. 33
      package.json
  8. 268
      test/caching.unit.js
  9. 5
      test/mocha.opts
  10. 313
      test/multi_caching.unit.js
  11. 51
      test/support.js

1
.gitignore

@ -0,0 +1 @@
node_module

6
index.js

@ -0,0 +1,6 @@
var cache = {
caching: require('./lib/caching'),
multi_caching: require('./lib/multi_caching')
};
module.exports = cache;

47
lib/caching.js

@ -0,0 +1,47 @@
var caching = function(args) {
args = args || {};
var self = {};
self.store_name = args.store || 'redis';
self.store = require('./stores/' + self.store_name).create(args);
/**
* Wraps a function in cache. I.e., the first time the function is run,
* its results are stored in cache so subsequent calls retrieve from cache
* instead of calling the function.
*
* @example
*
* var key = 'user_' + user_id;
* cache.run(key, function(cb) {
* user_adapter.get(user_id, cb);
* }, function(err, user) {
* console.log(user);
* });
*/
self.run = function(key, work, cb) {
self.store.get(key, function(err, result) {
if (err) { return cb(err); }
if (result) {
return cb(null, result);
}
work(function() {
var work_args = Array.prototype.slice.call(arguments, 0);
self.store.set(key, work_args[1], function(err) {
if (err) { return cb(err); }
cb.apply(null, work_args);
});
});
});
};
self.get = self.store.get;
self.set = self.store.set;
self.del = self.store.del;
return self;
};
module.exports = caching;

79
lib/multi_caching.js

@ -0,0 +1,79 @@
var async = require('async');
/**
* Module that lets you specify a hiearchy of caches.
*/
var multi_caching = function(caches) {
var self = {};
if (!Array.isArray(caches)) { throw new Error('multi_caching requires an array of caches'); }
function get_from_highest_priority_cache(key, cb) {
var i = 0;
async.forEachSeries(caches, function(cache, async_cb) {
cache.store.get(key, function(err, result) {
if (err) { return cb(err); }
if (result) {
// break out of async loop.
return cb(err, result, i);
}
i += 1;
async_cb(err);
});
}, cb);
}
function set_in_multiple_caches(caches, key, value, cb) {
async.forEach(caches, function(cache, async_cb) {
cache.store.set(key, value, async_cb);
}, cb);
};
/**
* Wraps a function in one or more caches.
* Has same API as regular caching module.
*
* If a key doesn't exist in any cache, it gets set in all caches.
* If a key exists in a high-priority (e.g., first) cache, it gets returned immediately
* without getting set in other lower-priority caches.
* If a key doesn't exist in a higher-priority cache but exists in a lower-priority
* cache, it gets set in all higher-priority caches.
*/
self.run = function(key, work, cb) {
get_from_highest_priority_cache(key, function(err, result, index) {
if (err) { return cb(err); }
if (result) {
var caches_to_set = caches.slice(0, index);
set_in_multiple_caches(caches_to_set, key, result, function(err) {
return cb(err, result);
});
} else {
work(function() {
var work_args = Array.prototype.slice.call(arguments, 0);
set_in_multiple_caches(caches, key, work_args[1], function(err) {
cb.apply(null, work_args);
});
});
}
});
};
self.set = function(key, value, cb) {
set_in_multiple_caches(caches, key, value, cb);
};
self.get = function(key, cb) {
get_from_highest_priority_cache(key, cb);
};
self.del = function(key, cb) {
async.forEach(caches, function(cache, async_cb) {
cache.store.del(key, async_cb);
}, cb);
};
return self;
};
module.exports = multi_caching;

38
lib/stores/memory.js

@ -0,0 +1,38 @@
var Lru = require("lru-cache")
var memory_store = function(args) {
args = args || {};
var db = args.db || 'cache';
var self = {};
var ttl = args.ttl;
var lru_opts = {
max: args.max || 500,
maxAge: ttl ? ttl * 1000 : null
};
var lru_cache = new Lru(lru_opts);
self.set = function(key, value, cb) {
lru_cache.set(key, value);
cb(null);
};
self.get = function(key, cb) {
cb(null, lru_cache.get(key));
};
self.del = function(key, cb) {
lru_cache.del(key);
cb(null);
};
return self;
};
var methods = {
create: function(args) {
return memory_store(args);
}
};
module.exports = methods;

68
lib/stores/redis.js

@ -0,0 +1,68 @@
/*
var redis_store = function(args) {
args = args || {};
var db = args.db || 'cache';
var self = {};
var ttl = args.ttl;
var client = djs.backends.redis.client({db: djs.settings.redis.dbs[db]});
self.set = function(key, value, cb) {
var val = JSON.stringify(value);
if (ttl) {
client.command('setex', {key: key, ttl: ttl, value: val}, cb);
} else {
client.command('set', {key: key, value: val}, cb);
}
};
self.get = function(key, cb) {
client.command('get', {key: key}, function(err, result) {
if (err) { return cb(err); }
if (result === undefined) { return cb(null, null); }
return cb(null, JSON.parse(result));
});
};
self.del = function(key, cb) {
client.command('del', {key: key}, cb);
};
return self;
};
*/
function redis_store(args) {
args = args || {};
var self = {};
var ttl = args.ttl;
self.client = require('redis').createClient(args.port, args.host, args);
self.get = function(key, cb) {
self.client.get(key, function(err, result) {
cb(err, JSON.parse(result));
});
};
self.set = function(key, value, cb) {
if (ttl) {
self.client.setex(key, ttl, JSON.stringify(value), cb);
} else {
self.client.set(key, JSON.stringify(value), cb);
}
};
self.del = function(key, cb) {
self.client.del(key, cb);
};
return self;
}
var methods = {
create: function(args) {
return redis_store(args);
}
};
module.exports = methods;

33
package.json

@ -0,0 +1,33 @@
{
"name": "cashew",
"version": "0.0.1",
"description": "Cache module for Node.JS",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "https://github.com/BryanDonovan/node-cashew.git"
},
"keywords": [
"cache",
"redis",
"lru-cache"
],
"author": "Bryan Donovan",
"license": "BSD",
"dependencies": {
"async": "0.1.22",
"hiredis": "0.1.14",
"lru-cache": "2.3.0",
"redis": "0.6.7"
},
"devDependencies": {
"Faker" : "0.5.6",
"istanbul": "0.1.29",
"jshint": "1.0.0",
"mocha": "1.8.1",
"sinon": "1.5.2"
}
}

268
test/caching.unit.js

@ -0,0 +1,268 @@
var assert = require('assert');
var sinon = require('sinon');
var redis = require('redis');
var Lru = require("lru-cache")
var support = require('./support');
var check_err = support.check_err;
var caching = require('../index').caching;
var memory_store = require('../lib/stores/memory');
function get_widget(name, cb) {
cb(null, {name: name});
}
describe("caching", function() {
var cache;
var key;
var ttl;
var name;
var value;
describe("get() and set()", function() {
context("using redis store", function() {
beforeEach(function() {
cache = caching({store: 'redis'});
key = support.random.string(20);
value = support.random.string();
});
it("lets us set and get data in cache", function(done) {
cache.set(key, value, function(err, result) {
check_err(err);
cache.get(key, function(err, result) {
assert.equal(result, value);
done();
});
});
});
});
context("using memory store", function() {
beforeEach(function() {
cache = caching({store: 'memory'});
key = support.random.string(20);
value = support.random.string();
});
it("lets us set and get data in cache", function(done) {
cache.set(key, value, function(err, result) {
check_err(err);
cache.get(key, function(err, result) {
assert.equal(result, value);
done();
});
});
});
});
});
describe("del()", function() {
['redis', 'memory'].forEach(function(store) {
context("using " + store + " store", function() {
beforeEach(function(done) {
cache = caching({store: store});
key = support.random.string(20);
value = support.random.string();
cache.set(key, value, function(err, result) {
check_err(err);
done();
});
});
it("deletes data from cache", function(done) {
cache.get(key, function(err, result) {
assert.equal(result, value);
cache.del(key, function(err, result) {
check_err(err);
cache.get(key, function(err, result) {
assert.ok(!result);
done();
});
});
});
});
});
});
});
describe("run()", function() {
context("using redis store", function() {
var redis_client;
before(function() {
redis_client = redis.createClient();
sinon.stub(redis, 'createClient').returns(redis_client);
});
beforeEach(function() {
cache = caching({store: 'redis'});
key = support.random.string(20);
name = support.random.string();
});
after(function() {
redis.createClient.restore();
});
it("calls back with the result of the wrapped function", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
done();
});
});
it("caches the result of the function in redis", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
redis_client.get(key, function(err, result) {
check_err(err);
assert.deepEqual(JSON.parse(result), {name: name});
done();
});
});
});
it("retrieves data from redis when available", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
redis_client.get(key, function(err, result) {
check_err(err);
sinon.spy(redis_client, 'get');
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
assert.ok(redis_client.get.calledWith(key));
redis_client.get.restore();
done();
});
});
});
});
context("when using ttl", function() {
beforeEach(function() {
ttl = 50;
cache = caching({store: 'redis', ttl: ttl});
});
it("expires cached result after ttl seconds", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
redis_client.ttl(key, function(err, result) {
check_err(err);
support.assert_within(result, ttl, 2);
done();
});
});
});
});
});
describe("using memory (lru-cache) store", function() {
var memory_store_stub;
beforeEach(function() {
ttl = 0.1;
memory_store_stub = memory_store.create({ttl: ttl});
sinon.stub(memory_store, 'create').returns(memory_store_stub);
cache = caching({store: 'memory', ttl: ttl});
key = support.random.string(20);
name = support.random.string();
});
afterEach(function() {
memory_store.create.restore();
});
it("calls back with the result of a function", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
done();
});
});
it("retrieves data from memory when available", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
memory_store_stub.get(key, function(err, result) {
check_err(err);
sinon.spy(memory_store_stub, 'get');
var func_called = false;
cache.run(key, function(cb) {
get_widget(name, function(err, result) {
func_called = true;
cb(err, result);
});
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
assert.ok(memory_store_stub.get.calledWith(key));
assert.ok(!func_called);
memory_store_stub.get.restore();
done();
});
});
});
});
it("expires cached result after ttl seconds", function(done) {
cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
memory_store_stub.get(key, function(err, result) {
check_err(err);
assert.deepEqual(widget, {name: name});
var func_called = false;
setTimeout(function () {
cache.run(key, function(cb) {
get_widget(name, function(err, result) {
func_called = true;
cb(err, result);
});
}, function(err, widget) {
check_err(err);
assert.ok(func_called);
assert.deepEqual(widget, {name: name});
done();
});
}, (ttl * 1000 + 10));
});
});
});
});
});
});

5
test/mocha.opts

@ -0,0 +1,5 @@
--reporter spec
--ui bdd
--globals state,newBlocks,params,type,__coverage__
--timeout 6500
--slow 200

313
test/multi_caching.unit.js

@ -0,0 +1,313 @@
var assert = require('assert');
var Lru = require("lru-cache")
var support = require('./support');
var check_err = support.check_err;
var caching = require('../index').caching;
var multi_caching = require('../index').multi_caching;
var memory_store = require('../lib/stores/memory');
function get_widget(name, cb) {
cb(null, {name: name});
}
describe("multi_caching", function() {
var redis_cache;
var memory_cache;
var memory_cache2;
var multi_cache;
var key;
var memory_ttl;
var redis_ttl;
var name;
beforeEach(function() {
memory_ttl = 0.1;
redis_ttl = 1;
memory_cache = caching({store: 'memory', ttl: memory_ttl});
memory_cache2 = caching({store: 'memory', ttl: memory_ttl});
redis_cache = caching({store: 'redis', ttl: redis_ttl});
key = support.random.string(20);
name = support.random.string();
});
describe("get(), set(), del()", function() {
var value;
beforeEach(function() {
multi_cache = multi_caching([memory_cache, redis_cache, memory_cache2]);
key = support.random.string(20);
value = support.random.string();
});
describe("set()", function() {
it("lets us set data in all caches", function(done) {
multi_cache.set(key, value, function(err, result) {
check_err(err);
memory_cache.get(key, function(err, result) {
assert.equal(result, value);
redis_cache.get(key, function(err, result) {
check_err(err);
assert.equal(result, value);
memory_cache2.get(key, function(err, result) {
check_err(err);
assert.equal(result, value);
done();
});
});
});
});
});
});
describe("get()", function() {
it("gets data from first cache that has it", function(done) {
redis_cache.set(key, value, function(err) {
check_err(err);
multi_cache.get(key, function(err, result) {
check_err(err);
assert.equal(result, value);
done();
});
});
});
});
describe("del()", function() {
it("lets us delete data in all caches", function(done) {
multi_cache.set(key, value, function(err, result) {
check_err(err);
multi_cache.del(key, function(err, result) {
check_err(err);
memory_cache.get(key, function(err, result) {
assert.ok(!result);
redis_cache.get(key, function(err, result) {
check_err(err);
assert.ok(!result);
memory_cache2.get(key, function(err, result) {
check_err(err);
assert.ok(!result);
done();
});
});
});
});
});
});
});
});
describe("run()", function() {
describe("using a single cache store", function() {
beforeEach(function() {
multi_cache = multi_caching([redis_cache]);
});
it("calls back with the result of a function", function(done) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
done();
});
});
});
describe("using two cache stores", function() {
beforeEach(function() {
multi_cache = multi_caching([memory_cache, redis_cache]);
});
it("calls back with the result of a function", function(done) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
done();
});
});
it("sets value in all caches", function(done) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
memory_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
redis_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
done();
});
});
});
});
context("when value exists in first store but not second", function() {
it("returns value from first store, does not set it in second", function(done) {
memory_cache.set(key, {name: name}, function(err) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
redis_cache.get(key, function(err, result) {
check_err(err);
assert.equal(result, null);
done();
});
});
});
});
});
context("when value exists in second store but not first", function() {
it("returns value from second store, sets it in first store", function(done) {
redis_cache.set(key, {name: name}, function(err) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
memory_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
done();
});
});
});
});
});
});
describe("using three cache stores", function() {
beforeEach(function() {
multi_cache = multi_caching([memory_cache, redis_cache, memory_cache2]);
});
it("calls back with the result of a function", function(done) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
done();
});
});
it("sets value in all caches", function(done) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
memory_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
redis_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
memory_cache2.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
done();
});
});
});
});
});
context("when value exists in first store only", function() {
it("returns value from first store, does not set it in second or third", function(done) {
memory_cache.set(key, {name: name}, function(err) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
redis_cache.get(key, function(err, result) {
check_err(err);
assert.equal(result, null);
memory_cache2.get(key, function(err, result) {
check_err(err);
assert.equal(result, null);
done();
});
});
});
});
});
});
context("when value exists in second store only", function() {
it("returns value from second store, sets it in first store, does not set third store", function(done) {
redis_cache.set(key, {name: name}, function(err) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
memory_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
memory_cache2.get(key, function(err, result) {
check_err(err);
assert.equal(result, null);
done();
});
});
});
});
});
});
context("when value exists in third store only", function() {
it("returns value from third store, sets it in first and second stores", function(done) {
memory_cache2.set(key, {name: name}, function(err) {
multi_cache.run(key, function(cb) {
get_widget(name, cb);
}, function(err, widget) {
check_err(err);
assert.deepEqual(widget, {name: name});
redis_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
memory_cache.get(key, function(err, result) {
check_err(err);
assert.deepEqual(result, {name: name});
done();
});
});
});
});
});
});
});
});
});

51
test/support.js

@ -0,0 +1,51 @@
var util = require('util');
var assert = require('assert');
var support = {
random: {
string: function(str_len) {
str_len = str_len || 8;
var chars = "abcdefghiklmnopqrstuvwxyz";
var random_str = '';
for (var i = 0; i < str_len; i++) {
var rnum = Math.floor(Math.random() * chars.length);
random_str += chars.substring(rnum, rnum + 1);
}
return random_str;
}
},
check_err: function(err) {
if (err) {
var msg;
if (err instanceof Error) {
msg = err;
} else if (err.msg) {
msg = err.msg;
} else {
msg = util.inspect(err);
}
var error = new Error(msg);
var stack = app_trace().split('\n');
stack.unshift(error.message);
error.stack = stack.join('\n');
throw error;
}
},
assert_between: function(actual, lower, upper) {
assert.ok(actual >= lower, "Expected " + actual + " to be >= " + lower);
assert.ok(actual <= upper, "Expected " + actual + " to be <= " + upper);
},
assert_within: function(actual, expected, delta) {
var lower = expected - delta;
var upper = expected + delta;
this.assert_between(actual, lower, upper);
}
};
module.exports = support;
Loading…
Cancel
Save