feat: #8824, cache refactor (#8851)

* feat: #8824, cache refactor

ability to disable caches
ability to download contents of cache
refactor cache modules to remove duplicated code

* fix: remove duplicate hit/miss tracking

check cacheEnabled in getUncachedKeys
v1.18.x
Barış Soner Uşaklı 4 years ago committed by GitHub
parent bcbc085497
commit f1f9b225b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,11 +1,11 @@
{
"post-cache": "Post Cache",
"posts-in-cache": "Posts in Cache",
"average-post-size": "Average Post Size",
"length-to-max": "Length / Max",
"percent-full": "%1% Full",
"post-cache-size": "Post Cache Size",
"items-in-cache": "Items in Cache",
"control-panel": "Control Panel",
"update-settings": "Update Cache Settings"
"update-settings": "Update Cache Settings",
"clear": "Clear",
"download": "Download",
"enabled": "Enabled"
}

@ -23,14 +23,14 @@ get:
type: number
percentFull:
type: number
avgPostSize:
type: number
hits:
type: string
misses:
type: string
hitRatio:
type: string
enabled:
type: boolean
groupCache:
type: object
properties:
@ -48,6 +48,8 @@ get:
type: string
hitRatio:
type: string
enabled:
type: boolean
localCache:
type: object
properties:
@ -59,14 +61,14 @@ get:
type: number
percentFull:
type: number
dump:
type: boolean
hits:
type: string
misses:
type: string
hitRatio:
type: string
enabled:
type: boolean
objectCache:
type: object
properties:
@ -84,6 +86,8 @@ get:
type: string
hitRatio:
type: string
enabled:
type: boolean
required:
- postCache
- groupCache

@ -15,6 +15,16 @@ define('admin/advanced/cache', function () {
ajaxify.refresh();
});
});
$('.checkbox').on('change', function () {
var input = $(this).find('input');
var flag = input.is(':checked');
var name = $(this).attr('data-name');
socket.emit('admin.cache.toggle', { name: name, enabled: flag }, function (err) {
if (err) {
return app.alertError(err.message);
}
});
});
};
return Cache;
});

@ -1,56 +1,9 @@
'use strict';
const LRU = require('lru-cache');
const pubsub = require('./pubsub');
const cacheCreate = require('./cacheCreate');
const cache = new LRU({
module.exports = cacheCreate({
name: 'local',
max: 4000,
maxAge: 0,
});
cache.hits = 0;
cache.misses = 0;
const cacheGet = cache.get;
const cacheDel = cache.del;
const cacheReset = cache.reset;
cache.get = function (key) {
const data = cacheGet.apply(cache, [key]);
if (data === undefined) {
cache.misses += 1;
} else {
cache.hits += 1;
}
return data;
};
cache.del = function (key) {
if (!Array.isArray(key)) {
key = [key];
}
pubsub.publish('local:cache:del', key);
key.forEach(key => cacheDel.apply(cache, [key]));
};
cache.reset = function () {
pubsub.publish('local:cache:reset');
localReset();
};
function localReset() {
cacheReset.apply(cache);
cache.hits = 0;
cache.misses = 0;
}
pubsub.on('local:cache:reset', function () {
localReset();
});
pubsub.on('local:cache:del', function (keys) {
if (Array.isArray(keys)) {
keys.forEach(key => cacheDel.apply(cache, [key]));
}
});
module.exports = cache;

@ -0,0 +1,91 @@
'use strict';
module.exports = function (opts) {
const LRU = require('lru-cache');
const pubsub = require('./pubsub');
const cache = new LRU(opts);
cache.name = opts.name;
cache.hits = 0;
cache.misses = 0;
cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true;
const cacheSet = cache.set;
const cacheGet = cache.get;
const cacheDel = cache.del;
const cacheReset = cache.reset;
cache.set = function (key, value) {
if (!cache.enabled) {
return;
}
cacheSet.apply(cache, [key, value]);
};
cache.get = function (key) {
if (!cache.enabled) {
return undefined;
}
const data = cacheGet.apply(cache, [key]);
if (data === undefined) {
cache.misses += 1;
} else {
cache.hits += 1;
}
return data;
};
cache.del = function (keys) {
if (!Array.isArray(keys)) {
keys = [keys];
}
pubsub.publish(cache.name + ':cache:del', keys);
keys.forEach(key => cacheDel.apply(cache, [key]));
};
cache.reset = function () {
pubsub.publish(cache.name + ':cache:reset');
localReset();
};
function localReset() {
cacheReset.apply(cache);
cache.hits = 0;
cache.misses = 0;
}
pubsub.on(cache.name + ':cache:reset', function () {
localReset();
});
pubsub.on(cache.name + ':cache:del', function (keys) {
if (Array.isArray(keys)) {
keys.forEach(key => cacheDel.apply(cache, [key]));
}
});
cache.getUnCachedKeys = function (keys, cachedData) {
if (!cache.enabled) {
return keys;
}
let data;
let isCached;
const unCachedKeys = keys.filter(function (key) {
data = cache.get(key);
isCached = data !== undefined;
if (isCached) {
cachedData[key] = data;
}
return !isCached;
});
var hits = keys.length - unCachedKeys.length;
var misses = keys.length - hits;
cache.hits += hits;
cache.misses += misses;
return unCachedKeys;
};
return cache;
};

@ -10,10 +10,8 @@ cacheController.get = function (req, res) {
const objectCache = require('../../database').objectCache;
const localCache = require('../../cache');
let avgPostSize = 0;
let percentFull = 0;
if (postCache.itemCount > 0) {
avgPostSize = parseInt((postCache.length / postCache.itemCount), 10);
percentFull = ((postCache.length / postCache.max) * 100).toFixed(2);
}
@ -23,10 +21,10 @@ cacheController.get = function (req, res) {
max: postCache.max,
itemCount: postCache.itemCount,
percentFull: percentFull,
avgPostSize: avgPostSize,
hits: utils.addCommas(String(postCache.hits)),
misses: utils.addCommas(String(postCache.misses)),
hitRatio: ((postCache.hits / (postCache.hits + postCache.misses) || 0)).toFixed(4),
enabled: postCache.enabled,
},
groupCache: {
length: groupCache.length,
@ -36,16 +34,17 @@ cacheController.get = function (req, res) {
hits: utils.addCommas(String(groupCache.hits)),
misses: utils.addCommas(String(groupCache.misses)),
hitRatio: (groupCache.hits / (groupCache.hits + groupCache.misses)).toFixed(4),
enabled: groupCache.enabled,
},
localCache: {
length: localCache.length,
max: localCache.max,
itemCount: localCache.itemCount,
percentFull: ((localCache.length / localCache.max) * 100).toFixed(2),
dump: req.query.debug ? JSON.stringify(localCache.dump(), null, 4) : false,
hits: utils.addCommas(String(localCache.hits)),
misses: utils.addCommas(String(localCache.misses)),
hitRatio: ((localCache.hits / (localCache.hits + localCache.misses) || 0)).toFixed(4),
enabled: localCache.enabled,
},
};
@ -58,8 +57,31 @@ cacheController.get = function (req, res) {
hits: utils.addCommas(String(objectCache.hits)),
misses: utils.addCommas(String(objectCache.misses)),
hitRatio: (objectCache.hits / (objectCache.hits + objectCache.misses)).toFixed(4),
enabled: objectCache.enabled,
};
}
res.render('admin/advanced/cache', data);
};
cacheController.dump = function (req, res, next) {
const caches = {
post: require('../../posts/cache'),
object: require('../../database').objectCache,
group: require('../../groups').cache,
local: require('../../cache'),
};
if (!caches[req.query.name]) {
return next();
}
const data = JSON.stringify(caches[req.query.name].dump(), null, 4);
res.setHeader('Content-disposition', 'attachment; filename= ' + req.query.name + '-cache.json');
res.setHeader('Content-type', 'application/json');
res.write(data, function (err) {
if (err) {
return next(err);
}
res.end();
});
};

@ -1,56 +1,11 @@
'use strict';
module.exports.create = function (name) {
var LRU = require('lru-cache');
var pubsub = require('../pubsub');
var cache = new LRU({
const cacheCreate = require('../cacheCreate');
return cacheCreate({
name: name + '-object',
max: 40000,
length: function () { return 1; },
maxAge: 0,
});
cache.misses = 0;
cache.hits = 0;
pubsub.on(name + ':hash:cache:del', function (keys) {
keys.forEach(key => cache.del(key));
});
pubsub.on(name + ':hash:cache:reset', function () {
cache.reset();
});
cache.delObjectCache = function (keys) {
if (!Array.isArray(keys)) {
keys = [keys];
}
pubsub.publish(name + ':hash:cache:del', keys);
keys.forEach(key => cache.del(key));
};
cache.resetObjectCache = function () {
pubsub.publish(name + ':hash:cache:reset');
cache.reset();
};
cache.getUnCachedKeys = function (keys, cachedData) {
let data;
let isCached;
const unCachedKeys = keys.filter(function (key) {
data = cache.get(key);
isCached = data !== undefined;
if (isCached) {
cachedData[key] = data;
}
return !isCached;
});
var hits = keys.length - unCachedKeys.length;
var misses = keys.length - hits;
cache.hits += hits;
cache.misses += misses;
return unCachedKeys;
};
return cache;
};

@ -29,7 +29,7 @@ module.exports = function (module) {
throw err;
}
cache.delObjectCache(key);
cache.del(key);
};
module.setObjectField = async function (key, field, value) {
@ -164,7 +164,7 @@ module.exports = function (module) {
});
await module.client.collection('objects').updateOne({ _key: key }, { $unset: data });
cache.delObjectCache(key);
cache.del(key);
};
module.incrObjectField = async function (key, field) {
@ -191,13 +191,13 @@ module.exports = function (module) {
bulk.find({ _key: key }).upsert().update({ $inc: increment });
});
await bulk.execute();
cache.delObjectCache(key);
cache.del(key);
const result = await module.getObjectsFields(key, [field]);
return result.map(data => data && data[field]);
}
const result = await module.client.collection('objects').findOneAndUpdate({ _key: key }, { $inc: increment }, { returnOriginal: false, upsert: true });
cache.delObjectCache(key);
cache.del(key);
return result && result.value ? result.value[field] : null;
};
};

@ -8,7 +8,7 @@ module.exports = function (module) {
module.emptydb = async function () {
await module.client.collection('objects').deleteMany({});
module.objectCache.resetObjectCache();
module.objectCache.reset();
};
module.exists = async function (key) {
@ -47,7 +47,7 @@ module.exports = function (module) {
return;
}
await module.client.collection('objects').deleteMany({ _key: key });
module.objectCache.delObjectCache(key);
module.objectCache.del(key);
};
module.deleteAll = async function (keys) {
@ -55,7 +55,7 @@ module.exports = function (module) {
return;
}
await module.client.collection('objects').deleteMany({ _key: { $in: keys } });
module.objectCache.delObjectCache(keys);
module.objectCache.del(keys);
};
module.get = async function (key) {
@ -96,7 +96,7 @@ module.exports = function (module) {
module.rename = async function (oldKey, newKey) {
await module.client.collection('objects').updateMany({ _key: oldKey }, { $set: { _key: newKey } });
module.objectCache.delObjectCache([oldKey, newKey]);
module.objectCache.del([oldKey, newKey]);
};
module.type = async function (key) {

@ -33,7 +33,7 @@ module.exports = function (module) {
await module.client.async.hmset(key, data);
}
cache.delObjectCache(key);
cache.del(key);
};
module.setObjectField = async function (key, field, value) {
@ -48,7 +48,7 @@ module.exports = function (module) {
await module.client.async.hset(key, field, value);
}
cache.delObjectCache(key);
cache.del(key);
};
module.getObject = async function (key) {
@ -146,7 +146,7 @@ module.exports = function (module) {
return;
}
await module.client.async.hdel(key, field);
cache.delObjectCache(key);
cache.del(key);
};
module.deleteObjectFields = async function (key, fields) {
@ -158,7 +158,7 @@ module.exports = function (module) {
return;
}
await module.client.async.hdel(key, fields);
cache.delObjectCache(key);
cache.del(key);
};
module.incrObjectField = async function (key, field) {
@ -182,7 +182,7 @@ module.exports = function (module) {
} else {
result = await module.client.async.hincrby(key, field, value);
}
cache.delObjectCache(key);
cache.del(key);
return Array.isArray(result) ? result.map(value => parseInt(value, 10)) : parseInt(result, 10);
};
};

@ -9,7 +9,7 @@ module.exports = function (module) {
module.emptydb = async function () {
await module.flushdb();
module.objectCache.resetObjectCache();
module.objectCache.reset();
};
module.exists = async function (key) {
@ -45,7 +45,7 @@ module.exports = function (module) {
module.delete = async function (key) {
await module.client.async.del(key);
module.objectCache.delObjectCache(key);
module.objectCache.del(key);
};
module.deleteAll = async function (keys) {
@ -53,7 +53,7 @@ module.exports = function (module) {
return;
}
await module.client.async.del(keys);
module.objectCache.delObjectCache(keys);
module.objectCache.del(keys);
};
module.get = async function (key) {
@ -77,7 +77,7 @@ module.exports = function (module) {
}
}
module.objectCache.delObjectCache([oldKey, newKey]);
module.objectCache.del([oldKey, newKey]);
};
module.type = async function (key) {

@ -1,48 +1,19 @@
'use strict';
var LRU = require('lru-cache');
var pubsub = require('../pubsub');
var cache = new LRU({
max: 40000,
maxAge: 0,
});
cache.hits = 0;
cache.misses = 0;
const cacheCreate = require('../cacheCreate');
module.exports = function (Groups) {
Groups.cache = cache;
pubsub.on('group:cache:reset', function () {
localReset();
});
pubsub.on('group:cache:del', function (data) {
if (data && data.groupNames) {
data.groupNames.forEach(function (groupName) {
cache.del(data.uid + ':' + groupName);
});
}
Groups.cache = cacheCreate({
name: 'group',
max: 40000,
maxAge: 0,
});
Groups.resetCache = function () {
pubsub.publish('group:cache:reset');
localReset();
};
function localReset() {
cache.reset();
cache.hits = 0;
cache.misses = 0;
}
Groups.clearCache = function (uid, groupNames) {
if (!Array.isArray(groupNames)) {
groupNames = [groupNames];
}
pubsub.publish('group:cache:del', { uid: uid, groupNames: groupNames });
groupNames.forEach(function (groupName) {
cache.del(uid + ':' + groupName);
});
const keys = groupNames.map(name => uid + ':' + name);
Groups.cache.del(keys);
};
};

@ -40,7 +40,7 @@ module.exports = function (Groups) {
db.deleteObjectFields('groupslug:groupname', fields),
removeGroupsFromPrivilegeGroups(groupNames),
]);
Groups.resetCache();
Groups.cache.reset();
plugins.fireHook('action:groups.destroy', { groups: groupsData });
};

@ -31,10 +31,8 @@ module.exports = function (Groups) {
const cacheKey = uid + ':' + groupName;
let isMember = Groups.cache.get(cacheKey);
if (isMember !== undefined) {
Groups.cache.hits += 1;
return isMember;
}
Groups.cache.misses += 1;
isMember = await db.isSortedSetMember('group:' + groupName + ':members', uid);
Groups.cache.set(cacheKey, isMember);
return isMember;
@ -88,10 +86,7 @@ module.exports = function (Groups) {
const isMember = Groups.cache.get(uid + ':' + groupName);
const isInCache = isMember !== undefined;
if (isInCache) {
Groups.cache.hits += 1;
cachedData[uid + ':' + groupName] = isMember;
} else {
Groups.cache.misses += 1;
}
return !isInCache;
}

@ -197,7 +197,7 @@ module.exports = function (Groups) {
old: oldName,
new: newName,
});
Groups.resetCache();
Groups.cache.reset();
};
async function updateMemberGroupTitles(oldName, newName) {

@ -1,14 +1,12 @@
'use strict';
var LRU = require('lru-cache');
var meta = require('../meta');
const cacheCreate = require('../cacheCreate');
const meta = require('../meta');
var cache = new LRU({
module.exports = cacheCreate({
name: 'post',
max: meta.config.postCacheSize,
length: function (n) { return n.length; },
maxAge: 0,
enabled: global.env === 'production',
});
cache.hits = 0;
cache.misses = 0;
module.exports = cache;

@ -57,13 +57,12 @@ module.exports = function (Posts) {
const cachedContent = cache.get(pid);
if (postData.pid && cachedContent !== undefined) {
postData.content = cachedContent;
cache.hits += 1;
return postData;
}
cache.misses += 1;
const data = await plugins.fireHook('filter:parse.post', { postData: postData });
data.postData.content = translator.escape(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
if (data.postData.pid) {
cache.set(pid, data.postData.content);
}
return data.postData;

@ -41,6 +41,9 @@ helpers.isUserAllowedTo = async function (privilege, uid, cid) {
};
async function isUserAllowedToCids(privilege, uid, cids) {
if (!privilege) {
return cids.map(() => false);
}
if (parseInt(uid, 10) <= 0) {
return await isSystemGroupAllowedToCids(privilege, uid, cids);
}

@ -61,6 +61,7 @@ function apiRoutes(router, middleware, controllers) {
router.get('/api/admin/users/csv', middleware.authenticate, helpers.tryRoute(controllers.admin.users.getCSV));
router.get('/api/admin/groups/:groupname/csv', middleware.authenticate, helpers.tryRoute(controllers.admin.groups.getCSV));
router.get('/api/admin/analytics', middleware.authenticate, helpers.tryRoute(controllers.admin.dashboard.getAnalytics));
router.get('/api/admin/advanced/cache/dump', middleware.authenticate, helpers.tryRoute(controllers.admin.cache.dump));
const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();

@ -8,3 +8,16 @@ SocketCache.clear = async function () {
require('../../groups').cache.reset();
require('../../cache').reset();
};
SocketCache.toggle = async function (socket, data) {
const caches = {
post: require('../../posts/cache'),
object: require('../../database').objectCache,
group: require('../../groups').cache,
local: require('../../cache'),
};
if (!caches[data.name]) {
return;
}
caches[data.name].enabled = data.enabled;
};

@ -6,7 +6,13 @@
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> [[admin/advanced/cache:post-cache]]</div>
<div class="panel-body">
<label>[[admin/advanced/cache:length-to-max]]</label><br/>
<div class="checkbox" data-name="post">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" {{{if postCache.enabled}}}checked{{{end}}}>
<span class="mdl-switch__label"><strong>[[admin/advanced/cache:enabled]]</strong></span>
</label>
</div>
<span>{postCache.length} / {postCache.max}</span><br/>
<div class="progress">
@ -21,16 +27,11 @@
<hr/>
<label>[[admin/advanced/cache:posts-in-cache]]</label><br/>
<span>{postCache.itemCount}</span><br/>
<label>[[admin/advanced/cache:average-post-size]]</label><br/>
<span>{postCache.avgPostSize}</span><br/>
<div class="form-group">
<label for="postCacheSize">[[admin/advanced/cache:post-cache-size]]</label>
<input id="postCacheSize" type="text" class="form-control" value="" data-field="postCacheSize">
</div>
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name=post" class="btn btn-default"><i class="fa fa-download"></i> [[admin/advanced/cache:download]]</a>
</div>
</div>
</div>
@ -40,7 +41,12 @@
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> Object Cache</div>
<div class="panel-body">
<label>[[admin/advanced/cache:length-to-max]]</label><br/>
<div class="checkbox" data-name="object">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" {{{if objectCache.enabled}}}checked{{{end}}}>
<span class="mdl-switch__label"><strong>[[admin/advanced/cache:enabled]]</strong></span>
</label>
</div>
<span>{objectCache.length} / {objectCache.max}</span><br/>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{objectCache.percentFull}" aria-valuemin="0" aria-valuemax="100" style="width: {objectCache.percentFull}%;">
@ -51,6 +57,7 @@
<label>Hits:</label> <span>{objectCache.hits}</span><br/>
<label>Misses:</label> <span>{objectCache.misses}</span><br/>
<label>Hit Ratio:</label> <span>{objectCache.hitRatio}</span><br/>
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name=object" class="btn btn-default"><i class="fa fa-download"></i> [[admin/advanced/cache:download]]</a>
</div>
</div>
</div>
@ -59,8 +66,12 @@
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> Group Cache</div>
<div class="panel-body">
<label>[[admin/advanced/cache:length-to-max]]</label><br/>
<div class="checkbox" data-name="group">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" {{{if groupCache.enabled}}}checked{{{end}}}>
<span class="mdl-switch__label"><strong>[[admin/advanced/cache:enabled]]</strong></span>
</label>
</div>
<span>{groupCache.length} / {groupCache.max}</span><br/>
<div class="progress">
@ -72,6 +83,7 @@
<label>Hits:</label> <span>{groupCache.hits}</span><br/>
<label>Misses:</label> <span>{groupCache.misses}</span><br/>
<label>Hit Ratio:</label> <span>{groupCache.hitRatio}</span><br/>
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name=group" class="btn btn-default"><i class="fa fa-download"></i> [[admin/advanced/cache:download]]</a>
</div>
</div>
</div>
@ -79,8 +91,12 @@
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-calendar-o"></i> Local Cache</div>
<div class="panel-body">
<label>[[admin/advanced/cache:length-to-max]]</label><br/>
<div class="checkbox" data-name="local">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" {{{if localCache.enabled}}}checked{{{end}}}>
<span class="mdl-switch__label"><strong>[[admin/advanced/cache:enabled]]</strong></span>
</label>
</div>
<span>{localCache.length} / {localCache.max}</span><br/>
<div class="progress">
@ -92,11 +108,7 @@
<label>Hits:</label> <span>{localCache.hits}</span><br/>
<label>Misses:</label> <span>{localCache.misses}</span><br/>
<label>Hit Ratio:</label> <span>{localCache.hitRatio}</span><br/>
<!-- IF localCache.dump -->
<pre>{localCache.dump}</pre>
<!-- ENDIF localCache.dump -->
<a href="{config.relative_path}/api/admin/advanced/cache/dump?name=local" class="btn btn-default"><i class="fa fa-download"></i> [[admin/advanced/cache:download]]</a>
</div>
</div>
</div>

@ -16,7 +16,7 @@ describe('meta', function () {
var herpUid;
before(function (done) {
Groups.resetCache();
Groups.cache.reset();
// Create 3 users: 1 admin, 2 regular
async.series([
async.apply(User.create, { username: 'foo', password: 'barbar' }), // admin

@ -168,7 +168,7 @@ async function setupMockDefaults() {
const meta = require('../../src/meta');
await db.emptydb();
require('../../src/groups').resetCache();
require('../../src/groups').cache.reset();
require('../../src/posts/cache').reset();
require('../../src/cache').reset();
winston.info('test_database flushed');

Loading…
Cancel
Save