Browse Source

Merge branch 'master' into develop

feature/nested-cache-fetch-fix
Bryan Donovan 10 years ago
parent
commit
ec64da3774
  1. 53
      lib/caching.js
  2. 75
      lib/multi_caching.js
  3. 17
      test/caching.unit.js
  4. 105
      test/multi_caching.unit.js

53
lib/caching.js

@ -1,4 +1,6 @@
/*jshint maxcomplexity:15*/
var domain = require('domain');
var caching = function (args) {
args = args || {};
var self = {};
@ -40,38 +42,43 @@ var caching = function (args) {
ttl = undefined;
}
if (self.queues[key]) {
self.queues[key].push({cb: cb, domain: process.domain});
return;
}
self.queues[key] = [{cb: cb, domain: process.domain}];
function fillCallbacks(err, data) {
self.queues[key].forEach(function(task) {
var taskDomain = task.domain || domain.create();
taskDomain.bind(task.cb)(err, data);
});
delete self.queues[key];
}
self.store.get(key, function (err, result) {
if (err && (!self.ignoreCacheErrors)) {
cb(err);
fillCallbacks(err);
} else if (result) {
cb.call(cb, null, result);
} else if (self.queues[key]) {
self.queues[key].push(cb);
fillCallbacks(null, result);
} else {
self.queues[key] = [cb];
work(function () {
var work_args = Array.prototype.slice.call(arguments, 0);
if (work_args[0]) { // assume first arg is an error
self.queues[key].forEach(function (done) {
done.call(null, work_args[0]);
});
delete self.queues[key];
domain
.create()
.on('error', function(err) {
fillCallbacks(err);
})
.bind(work)(function (err, data) {
if (err) {
fillCallbacks(err);
return;
}
// Subsequently assume second arg is result.
self.store.set(key, work_args[1], ttl, function (err) {
self.store.set(key, data, ttl, function (err) {
if (err && (!self.ignoreCacheErrors)) {
self.queues[key].forEach(function (done) {
done.call(null, err);
});
fillCallbacks(err);
} else {
self.queues[key].forEach(function (done) {
done.apply(null, work_args);
});
fillCallbacks(null, data);
}
delete self.queues[key];
});
});
}

75
lib/multi_caching.js

@ -1,4 +1,5 @@
var async = require('async');
var domain = require('domain');
/**
* Module that lets you specify a hierarchy of caches.
@ -31,6 +32,29 @@ var multi_caching = function (caches) {
}, cb);
}
/**
* Looks for an item in cache tiers.
*
* When a key is found in a lower cache, all higher levels are updated
*/
self.get_and_pass_up = function(key, cb) {
get_from_highest_priority_cache(key, function(err, result, index) {
if (err) {
return cb(err);
}
cb(err, result);
if (result !== undefined && index) {
var cachesToUpdate = caches.slice(0, index);
async.forEach(cachesToUpdate, function(cache, async_cb) {
cache.set(key, result, result.ttl, async_cb);
});
}
});
};
/**
* Wraps a function in one or more caches.
* Has same API as regular caching module.
@ -47,9 +71,24 @@ var multi_caching = function (caches) {
ttl = undefined;
}
if (self.queues[key]) {
self.queues[key].push({cb: cb, domain: process.domain});
return;
}
self.queues[key] = [{cb: cb, domain: process.domain}];
function fillCallbacks(err, data) {
self.queues[key].forEach(function(task) {
var taskDomain = task.domain || domain.create();
taskDomain.bind(task.cb)(err, data);
});
delete self.queues[key];
}
get_from_highest_priority_cache(key, function (err, result, index) {
if (err) {
return cb(err);
return fillCallbacks(err);
} else if (result) {
var caches_to_update = caches.slice(0, index);
var opts = {
@ -58,38 +97,30 @@ var multi_caching = function (caches) {
ttl: ttl
};
set_in_multiple_caches(caches_to_update, opts, function (err) {
cb(err, result);
fillCallbacks(err, result);
});
} else if (self.queues[key]) {
self.queues[key].push(cb);
} else {
self.queues[key] = [cb];
work(function () {
var work_args = Array.prototype.slice.call(arguments, 0);
if (work_args[0]) { // assume first arg is an error
self.queues[key].forEach(function (done) {
done.call(null, work_args[0]);
});
delete self.queues[key];
domain
.create()
.on('error', function(err) {
fillCallbacks(err);
})
.bind(work)(function (err, data) {
if (err) {
fillCallbacks(err);
return;
}
var opts = {
key: key,
value: work_args[1],
value: data,
ttl: ttl
};
set_in_multiple_caches(caches, opts, function (err) {
if (err) {
self.queues[key].forEach(function (done) {
done.call(null, err);
});
delete self.queues[key];
return;
fillCallbacks(err);
} else {
fillCallbacks(null, data);
}
self.queues[key].forEach(function (done) {
done.apply(null, work_args);
});
delete self.queues[key];
});
});
}

17
test/caching.unit.js

@ -357,6 +357,23 @@ describe("caching", function () {
});
});
context("when an error is thrown in the work function", function () {
var fake_error;
beforeEach(function() {
fake_error = new Error(support.random.string());
});
it("bubbles up that error", function (done) {
cache.wrap(key, function () {
throw fake_error;
}, ttl, function (err) {
assert.equal(err, fake_error);
done();
});
});
});
context("when store.get() calls back with an error", function () {
context("and ignoreCacheErrors is not set (default is false)", function () {
it("bubbles up that error", function (done) {

105
test/multi_caching.unit.js

@ -178,6 +178,94 @@ describe("multi_caching", function () {
});
});
describe("get_and_pass_up()", function () {
var value;
var key;
describe("using a single cache store", function () {
beforeEach(function () {
multi_cache = multi_caching([memory_cache3]);
key = support.random.string(20);
value = support.random.string();
});
it("gets data from first cache that has it", function (done) {
memory_cache3.set(key, value, ttl, function (err) {
check_err(err);
multi_cache.get_and_pass_up(key, function (err, result) {
check_err(err);
assert.equal(result, value);
done();
});
});
});
});
describe("when value is not found in any cache", function() {
var response;
beforeEach(function(done) {
key = support.random.string(10);
sinon.spy(memory_cache, 'set');
sinon.spy(memory_cache2, 'set');
sinon.spy(memory_cache3, 'set');
multi_cache.get_and_pass_up(key, function (err, result) {
check_err(err);
response = result;
done();
});
});
afterEach(function() {
memory_cache.set.restore();
memory_cache2.set.restore();
memory_cache3.set.restore();
});
it("calls back with undefined", function() {
assert.strictEqual(response, undefined);
});
it("does not set anything in caches", function(done) {
process.nextTick(function () {
assert.ok(memory_cache.set.notCalled);
assert.ok(memory_cache2.set.notCalled);
assert.ok(memory_cache3.set.notCalled);
done();
});
});
});
describe("using multi cache store", function () {
beforeEach(function () {
multi_cache = multi_caching([memory_cache,memory_cache2,memory_cache3]);
key = support.random.string(20);
value = support.random.string();
});
it("checks to see if higher levels have item", function (done) {
memory_cache3.set(key, value, ttl, function (err) {
check_err(err);
multi_cache.get_and_pass_up(key, function (err, result) {
check_err(err);
assert.equal(result, value);
process.nextTick(function() {
memory_cache.get(key, function (err, result) {
assert.equal(result, value);
check_err(err);
done();
});
});
});
});
});
});
});
describe("wrap()", function () {
describe("using a single cache store", function () {
beforeEach(function () {
@ -449,6 +537,23 @@ describe("multi_caching", function () {
memory_store.create.restore();
});
context("when an error is thrown in the work function", function () {
var fake_error;
beforeEach(function() {
fake_error = new Error(support.random.string());
});
it("bubbles up that error", function (done) {
multi_cache.wrap(key, function () {
throw fake_error;
}, ttl, function (err) {
assert.equal(err, fake_error);
done();
});
});
});
context("when store.get() calls back with an error", function () {
it("bubbles up that error", function (done) {
var fake_error = new Error(support.random.string());

Loading…
Cancel
Save