lru-cache-for-clusters-as-promised
Version:
LRU Cache that is safe for clusters, based on `lru-cache`. Save memory by only caching items on the main thread via a promisified interface.
801 lines (783 loc) • 23.9 kB
JavaScript
const config = require('./test-config');
const cluster = require('cluster');
const { parse, stringify } = require('flatted');
const should = require('should');
const LRUCacheForClustersAsPromised = require('../../lru-cache-for-clusters-as-promised');
const LRUCache = require('lru-cache');
const member = cluster.isWorker ? 'worker' : 'master';
// create a default
new LRUCacheForClustersAsPromised();
/**
* Test class definitions for clusterd and non-clustered environments
* @param {LRUCacheForClustersAsPromised} cache The cache that the test should be run against.
* @return {void} Node style callback
*/
function TestUtils(cache) {
const object = {
foo:
'bar barbarbar barbarbar barbarbar barbarbar barbarbar barbarbar barbarbar barbarbar barbar',
};
const pairs = {
foo: 'bar',
bizz: 'buzz',
obj: {
hi: 'im an object',
},
};
const keys = Object.keys(pairs);
return {
clusterTests: {
hi: 'not respond to messages that are from somewhere else',
timeout: 'timeout',
reject: 'timeout with reject',
},
tests: {
executeSetGet: 'try to call set via the execute() option',
executeFail: 'execute fail',
getLruCachesOnMaster:
'getLruCaches to return the underlying LRUCaches from master, throw error on worker',
getCache: 'get underlying LRUCache for promisified version',
mSet: 'mSet values',
mSetNull: 'mSet null pairs',
mGet: 'mGet values',
mGetNull: 'mGet with null keys',
mGetAndSetObjects: 'mGetObjects and mSetObjects',
mDel: 'mGet keys',
mDelNull: 'mDel with null keys',
objects: 'get and set objects',
null_objects: 'null objects should be ok',
undefined_objects: 'undefined objects should be ok',
circular_objects: 'circular objects should be ok',
miss_undefined: 'missing objects should return undefined',
pruneJob: 'prune cache using cron job',
pruneJob2: 'prune cache using cron job, longer than test',
set: 'set(key, value)',
get: 'get(key)',
del: 'del(key)',
incr: 'incr(key) - increment value by 1',
incr2: 'incr(key, 2) - increment value by 2',
decr: 'decr(key) - decrement value by 1',
decr2: 'decr(key, 2) - decrement value by 2',
peek:
'peek(key) - get a cache value but do not update access time for LRU',
has: 'has(key) - check if a key exists',
length: 'length()',
itemCount: 'itemCount()',
reset: 'reset()',
keys: 'keys()',
values: 'values()',
prune: 'prune()',
dump: 'dump()',
addFour: 'add four keys and have the first fall out',
addFourAccessOne:
'add four keys and then access the first so the second falls out',
getMax: 'max()',
getMaxAge: 'maxAge()',
getStale: 'stale()',
getAllowStale: 'allowStale()',
setMax: 'max(10)',
setMaxAge: 'maxAge(10)',
setStale: 'stale(true)',
setAllowStale: 'allowStale(true)',
properties: 'update cache properties',
getInstance:
'get an instance asynchronously, ensures cache has been created on the server',
},
executeSetGet: async (cb) => {
try {
await cache.execute('set', 1, 'execute');
const value = await cache.execute('get', 1);
should(value).equal('execute');
cb(null, true);
} catch (err) {
cb(err);
}
},
executeFail: async (cb) => {
try {
try {
await cache.execute('borked', 1, 'execute');
} catch (err) {
should(err.message).equal(
'LRUCache.borked() is not a valid function'
);
}
cb(null, true);
} catch (err) {
cb(err);
}
},
getLruCachesOnMaster: async (cb) => {
try {
try {
const yo = 'yo yo yo';
// get the default cache and set the value using a promise
const defCache = new LRUCacheForClustersAsPromised();
await defCache.set(1, yo);
// get all the caches and check the default namespace
const caches = LRUCacheForClustersAsPromised.getAllCaches();
should(typeof caches.default).not.equal('undefined');
should(caches.default instanceof LRUCache).equal(true);
should(caches.default.allowStale).equal(false);
should(caches.default.maxAge).equal(0);
// get the value we set synchronously
const value = caches.default.get(1);
should(value).equal(yo);
} catch (err) {
if (!cluster.isWorker) {
throw err;
}
should(err.message).containEql('LRUCacheForClustersAsPromised');
}
cb(null, true);
} catch (err) {
cb(err);
}
},
getCache: async (cb) => {
try {
try {
const foo = 'foo foo foo';
const defCache = new LRUCacheForClustersAsPromised();
await defCache.set(1, foo);
const cache = defCache.getCache();
should(cache.get(1)).equal(foo);
} catch (err) {
if (!cluster.isWorker) {
throw err;
}
}
cb(null, true);
} catch (err) {
cb(err);
}
},
mSet: async (cb) => {
try {
await cache.mSet(pairs);
const value = await cache.get(keys[0]);
should(value).equal(pairs[keys[0]]);
cb(null, true);
} catch (err) {
cb(err);
}
},
mSetNull: async (cb) => {
try {
await cache.mSet(null);
await cache.mSet('string');
await cache.mSet(['array']);
cb(null, true);
} catch (err) {
cb(err);
}
},
mGet: async (cb) => {
try {
await cache.mSet(pairs);
const values = await cache.mGet(keys);
should(typeof values).not.equal('undefined');
should(values.bizz).equal(pairs.bizz);
should(values.foo).equal(pairs.foo);
cb(null, true);
} catch (err) {
cb(err);
}
},
mGetAndSetObjects: async (cb) => {
try {
await cache.mSetObjects(pairs);
const values = await cache.mGetObjects(keys);
should(values.bizz).deepEqual(pairs.bizz);
should(values.foo).deepEqual(pairs.foo);
cb(null, true);
} catch (err) {
cb(err);
}
},
mGetNull: async (cb) => {
try {
let values = await cache.mGet('string');
should(values).deepEqual({});
values = await cache.mGet(null);
should(values).deepEqual({});
cb(null, true);
} catch (err) {
cb(err);
}
},
mDel: async (cb) => {
try {
await cache.mSet(pairs);
await cache.mDel(keys);
const value = await cache.get(keys[0]);
should(typeof value).equal('undefined');
cb(null, true);
} catch (err) {
cb(err);
}
},
mDelNull: async (cb) => {
try {
await cache.mSet(pairs);
await cache.mDel(null);
const value = await cache.get(keys[0]);
should(value).equal(pairs[keys[0]]);
cb(null, true);
} catch (err) {
cb(err);
}
},
objects: async (cb) => {
try {
await cache.setObject(1, object);
const obj = await cache.getObject(1);
should(obj).not.equal(null);
should(obj.foo).equal(object.foo);
cb(null, true);
} catch (err) {
cb(err);
}
},
undefined_objects: async (cb) => {
try {
let object;
await cache.setObject(1, object);
const obj = await cache.getObject(1);
should(typeof obj).equal('undefined');
cb(null, true);
} catch (err) {
cb(err);
}
},
null_objects: async (cb) => {
try {
let object = null;
await cache.setObject(1, object);
const obj = await cache.getObject(1);
should(obj).equal(null);
cb(null, true);
} catch (err) {
cb(err);
}
},
circular_objects: async (cb) => {
try {
// this cache uses the flatted parse and stringify
const cacheCircular = new LRUCacheForClustersAsPromised({
namespace: 'circular-cache',
max: 3,
parse,
stringify,
});
// create a circular dependency
const a = { b: null };
const b = { a };
b.a.b = b;
// see if we can set and then extract the circular object
await cacheCircular.setObject(1, a);
const obj = await cacheCircular.getObject(1);
should(obj).deepEqual(a);
should(obj.b).deepEqual(b);
cb(null, true);
} catch (err) {
cb(err);
}
},
miss_undefined: async (cb) => {
try {
const obj = await cache.getObject(1);
should(typeof obj).equal('undefined');
cb(null, true);
} catch (err) {
cb(err);
}
},
hi: async (cb) => {
try {
let responded = false;
const callback = (response) => {
if (!responded) {
responded = true;
should(response).equal('hello');
cb(null, true);
}
};
process.on('message', (response) => callback && callback(response));
process.send('hi');
} catch (err) {
cb(err);
}
},
timeout: async (cb) => {
try {
const cacheBad = new LRUCacheForClustersAsPromised({
max: 1,
stale: false,
timeout: 1,
namespace: `bad-cache-resolve-${member}`,
});
let large = '1234567890';
for (let i = 0; i < 17; i += 1) {
large += large;
}
const result = await cacheBad.get(`bad-cache-key-${large}`);
cb(null, result);
} catch (err) {
cb(err);
}
},
reject: async (cb) => {
try {
const cacheBad = new LRUCacheForClustersAsPromised({
max: 2,
stale: false,
timeout: 1,
failsafe: 'reject',
namespace: `bad-cache-reject-${member}`,
});
let large = '1234567890';
for (let i = 0; i < 17; i += 1) {
large += large;
}
await cacheBad.get(`bad-cache-key-${large}`);
cb('fail');
} catch (err) {
cb(null, true);
}
},
pruneJob: async (cb) => {
try {
const namespace = `pruned-cache-${member}-${Math.random()}`;
const prunedCache = new LRUCacheForClustersAsPromised({
max: 10,
stale: true,
maxAge: 100,
namespace,
prune: '*/1 * * * * *',
});
// maybe delay the start to sync with cron
const now = new Date();
const delay =
now.getMilliseconds() < 800 ? 0 : 1000 - now.getMilliseconds() + 10;
setTimeout(async () => {
await prunedCache.set(config.args.one, config.args.one, 200);
await prunedCache.set(config.args.two, config.args.two, 1200);
const itemCount = await prunedCache.itemCount();
// we should see 2 items in the cache
should(itemCount).equal(2);
// check again in 1100 ms
setTimeout(async () => {
// one of the items should have been removed based on the expiration
const itemCount2 = await prunedCache.itemCount();
try {
should(itemCount2).equal(1);
new LRUCacheForClustersAsPromised({
namespace,
prune: false,
});
return cb(null, true);
} catch (err) {
return cb(err);
}
}, 1100);
}, delay);
} catch (err) {
cb(err);
}
},
pruneJob2: async (cb) => {
try {
const namespace = `pruned-cache-${member}-2-${Math.random()}`;
// create it with 1 sec pruning
new LRUCacheForClustersAsPromised({
namespace,
prune: '*/1 * * * * *',
});
// update it to run every 10 secs
const prunedCache = new LRUCacheForClustersAsPromised({
namespace,
prune: '*/5 * * * * *',
});
// maybe delay the start to sync with cron
const now = new Date();
const delay =
now.getSeconds() % 5 < 4 ? 0 : 1000 - now.getMilliseconds() + 10;
setTimeout(async () => {
await prunedCache.set(config.args.one, config.args.one, 200);
await prunedCache.set(config.args.two, config.args.two, 1200);
const itemCount = await prunedCache.itemCount();
// we should see 2 items in the cache
should(itemCount).equal(2);
// check again in 1100 ms
setTimeout(async () => {
// both items should be there after they are expired
const itemCount2 = await prunedCache.itemCount();
try {
should(itemCount2).equal(2);
// disable prune job
await LRUCacheForClustersAsPromised.getInstance({
namespace,
prune: false,
});
return cb(null, true);
} catch (err) {
return cb(err);
}
}, 1000);
}, delay);
} catch (err) {
cb(err);
}
},
set: async (cb) => {
try {
const result = await cache.set(config.args.one, config.args.one);
cb(null, result);
} catch (err) {
cb(err);
}
},
get: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
const result = await cache.get(config.args.one);
should(result).equal(config.args.one);
cb(null, result);
} catch (err) {
cb(err);
}
},
del: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
await cache.del(config.args.one);
const result = await cache.get(config.args.one);
should(typeof result).equal('undefined');
cb(null, result);
} catch (err) {
cb(err);
}
},
incr: async (cb) => {
try {
const value = await cache.incr(config.args.one);
should(value).eql(1);
const value2 = await cache.incr(config.args.one);
should(value2).eql(2);
cb(null, true);
} catch (err) {
cb(err);
}
},
incr2: async (cb) => {
try {
const amount = 2;
const value = await cache.incr(config.args.one, amount);
should(value).eql(2);
const value2 = await cache.incr(config.args.one, amount);
should(value2).eql(4);
cb(null, true);
} catch (err) {
cb(err);
}
},
decr: async (cb) => {
try {
const value = await cache.decr(config.args.one);
should(value).eql(-1);
const value2 = await cache.decr(config.args.one);
should(value2).eql(-2);
cb(null, true);
} catch (err) {
cb(err);
}
},
decr2: async (cb) => {
try {
const amount = 2;
const value = await cache.decr(config.args.one, amount);
should(value).eql(-2);
const value2 = await cache.decr(config.args.one, amount);
should(value2).eql(-4);
cb(null, true);
} catch (err) {
cb(err);
}
},
peek: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
await cache.set(config.args.two, config.args.two);
await cache.set(config.args.three, config.args.three);
const result = await cache.peek(config.args.one);
should(result).equal(config.args.one);
await cache.set(config.args.four, config.args.four);
const result2 = await cache.get(config.args.one);
should(typeof result2).equal('undefined');
cb(null, true);
} catch (err) {
cb(err);
}
},
has: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
const has = await cache.has(config.args.one);
should(has).equal(true);
cb(null, true);
} catch (err) {
cb(err);
}
},
length: async (cb) => {
try {
await cache.set(config.args.two, config.args.two);
await cache.set(config.args.three, config.args.three);
const length = await cache.length();
should(length).equal(2);
cb(null, true);
} catch (err) {
cb(err);
}
},
itemCount: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
const itemCount = await cache.itemCount();
should(itemCount).equal(1);
cb(null, true);
} catch (err) {
cb(err);
}
},
reset: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
const result = await cache.get(config.args.one);
should(typeof result).equal('string');
await cache.reset();
const result2 = await cache.get(config.args.one);
should(typeof result2).equal('undefined');
cb(null, true);
} catch (err) {
cb(err);
}
},
keys: async (cb) => {
try {
const result = await cache.set(config.args.one, config.args.one);
should(result).equal(true);
const keys = await cache.keys();
should(keys.length).equal(1);
should(keys[0]).equal(config.args.one);
cb(null, true);
} catch (err) {
cb(err);
}
},
values: async (cb) => {
try {
await cache.set(config.args.two, config.args.two);
const values = await cache.values();
should(values).deepEqual([config.args.two]);
cb(null, true);
} catch (err) {
cb(err);
}
},
prune: async (cb) => {
try {
await cache.set(config.args.one, config.args.one);
await cache.prune();
const itemCount = await cache.itemCount();
should(itemCount).equal(1);
cb(null, true);
} catch (err) {
cb(err);
}
},
dump: async (cb) => {
try {
await cache.set(config.args.one, config.args.two);
const dump = await cache.dump();
should(dump[0].k).equal(config.args.one);
should(dump[0].v).equal(config.args.two);
cb(null, true);
} catch (err) {
cb(err);
}
},
getMax: async (cb) => {
try {
const max = await cache.max();
should(max).equal(3);
cb(null, true);
} catch (err) {
cb(err);
}
},
getMaxAge: async (cb) => {
try {
await cache.maxAge(20);
const maxAge = await cache.maxAge();
should(maxAge).equal(20);
cb(null, true);
} catch (err) {
cb(err);
}
},
getStale: async (cb) => {
try {
const stale = await cache.stale();
should(stale).equal(false);
cb(null, true);
} catch (err) {
cb(err);
}
},
getAllowStale: async (cb) => {
try {
const stale = await cache.allowStale();
should(stale).equal(false);
cb(null, true);
} catch (err) {
cb(err);
}
},
setMax: async (cb) => {
try {
const max = await cache.max(10000);
should(max).equal(10000);
cb(null, true);
} catch (err) {
cb(err);
}
},
setMaxAge: async (cb) => {
try {
const maxAge = await cache.maxAge(10);
should(maxAge).equal(10);
cb(null, true);
} catch (err) {
cb(err);
}
},
setStale: async (cb) => {
try {
const stale = await cache.stale(true);
should(stale).equal(true);
cb(null, true);
} catch (err) {
cb(err);
}
},
setAllowStale: async (cb) => {
try {
const stale = await cache.allowStale(true);
should(stale).equal(true);
cb(null, true);
} catch (err) {
cb(err);
}
},
properties: async (cb) => {
try {
const propsCache = new LRUCacheForClustersAsPromised({
namespace: 'props-cache',
max: 1,
maxAge: 100000,
stale: true,
});
should(await propsCache.allowStale()).equal(true);
should(await propsCache.max()).equal(1);
should(await propsCache.maxAge()).equal(100000);
const propsCache2 = new LRUCacheForClustersAsPromised({
namespace: 'props-cache',
max: 10101,
stale: false,
});
should(await propsCache2.allowStale()).equal(false);
should(await propsCache2.max()).equal(10101);
should(await propsCache2.maxAge()).equal(100000);
const propsCache3 = new LRUCacheForClustersAsPromised({
namespace: 'props-cache',
maxAge: 1000,
});
should(await propsCache3.allowStale()).equal(false);
should(await propsCache3.max()).equal(10101);
should(await propsCache3.maxAge()).equal(1000);
cb(null, true);
} catch (err) {
cb(err);
}
},
getInstance: async (cb) => {
try {
const propsCache = await LRUCacheForClustersAsPromised.getInstance({
namespace: 'props-cache',
max: 1,
maxAge: 100000,
stale: true,
});
should(await propsCache.allowStale()).equal(true);
should(await propsCache.max()).equal(1);
should(await propsCache.maxAge()).equal(100000);
const propsCache2 = await LRUCacheForClustersAsPromised.getInstance({
namespace: 'props-cache',
max: 10101,
stale: false,
});
should(await propsCache2.allowStale()).equal(false);
should(await propsCache2.max()).equal(10101);
should(await propsCache2.maxAge()).equal(100000);
const propsCache3 = await LRUCacheForClustersAsPromised.getInstance({
namespace: 'props-cache',
maxAge: 1000,
});
should(await propsCache3.allowStale()).equal(false);
should(await propsCache3.max()).equal(10101);
should(await propsCache3.maxAge()).equal(1000);
cb(null, true);
} catch (err) {
cb(err);
}
},
addFour: async (cb) => {
try {
const value = await cache.set(config.args.one, config.args.one);
should(value).equal(true);
await cache.set(config.args.two, config.args.two);
await cache.set(config.args.three, config.args.three);
await cache.set(config.args.four, config.args.four);
const result = await cache.get(config.args.one);
should(typeof result).equal('undefined');
const result2 = await cache.get(config.args.four);
should(result2).equal(config.args.four);
cb(null, true);
} catch (err) {
cb(err);
}
},
addFourAccessOne: async (cb) => {
try {
const value = await cache.set(config.args.one, config.args.one);
should(value).equal(true);
const value2 = await cache.set(config.args.two, config.args.two);
should(value2).equal(true);
const value3 = await cache.set(config.args.three, config.args.three);
should(value3).equal(true);
const value4 = await cache.get(config.args.one);
should(value4).equal(config.args.one);
const value5 = await cache.set(config.args.four, config.args.four);
should(value5).equal(true);
const result = await cache.get(config.args.one);
should(result).equal(config.args.one);
cb(null, true);
} catch (err) {
cb(err);
}
},
};
}
module.exports = TestUtils;