From 00a066985a2d3171393a5b0fa5dff1491b55e7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 27 Nov 2018 19:38:28 -0500 Subject: [PATCH] cache categories:cid and cid::children these rarely change, no need to go to db for them --- src/cache.js | 56 +++++++++++++++++++++++++++++ src/categories/create.js | 8 +++-- src/categories/data.js | 2 +- src/categories/delete.js | 14 +++++++- src/categories/index.js | 36 ++++++++++++++++--- src/categories/update.js | 10 +++++- src/controllers/admin/cache.js | 13 +++++-- src/controllers/admin/homepage.js | 3 +- src/controllers/admin/privileges.js | 3 +- src/socket.io/admin/categories.js | 3 +- src/socket.io/categories.js | 8 ++--- src/user/categories.js | 10 ++---- src/user/index.js | 12 ++++--- src/views/admin/advanced/cache.tpl | 31 +++++++++++----- test/mocks/databasemock.js | 2 ++ 15 files changed, 168 insertions(+), 43 deletions(-) create mode 100644 src/cache.js diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 000000000000..6e54b7e0f1da --- /dev/null +++ b/src/cache.js @@ -0,0 +1,56 @@ +'use strict'; + +var LRU = require('lru-cache'); +var pubsub = require('./pubsub'); + +var cache = LRU({ + max: 1000, + 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; diff --git a/src/categories/create.js b/src/categories/create.js index cd8f7bdfe764..e144bcad05ee 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -7,6 +7,7 @@ var groups = require('../groups'); var plugins = require('../plugins'); var privileges = require('../privileges'); var utils = require('../utils'); +var cache = require('../cache'); module.exports = function (Categories) { Categories.create = function (data, callback) { @@ -82,6 +83,7 @@ module.exports = function (Categories) { ], next); }, function (results, next) { + cache.del(['categories:cid', 'cid:' + parentCid + ':children']); if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { return Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid, next); } @@ -153,9 +155,11 @@ module.exports = function (Categories) { const newParent = parseInt(results.source.parentCid, 10) || 0; if (copyParent) { tasks.push(async.apply(db.sortedSetRemove, 'cid:' + oldParent + ':children', toCid)); - } - if (copyParent) { tasks.push(async.apply(db.sortedSetAdd, 'cid:' + newParent + ':children', results.source.order, toCid)); + tasks.push(function (next) { + cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); + setImmediate(next); + }); } destination.description = results.source.description; diff --git a/src/categories/data.js b/src/categories/data.js index ff38d988b342..282b3e2c345a 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -56,7 +56,7 @@ module.exports = function (Categories) { Categories.getAllCategoryFields = function (fields, callback) { async.waterfall([ - async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + async.apply(Categories.getAllCidsFromSet, 'categories:cid'), function (cids, next) { Categories.getCategoriesFields(cids, fields, next); }, diff --git a/src/categories/delete.js b/src/categories/delete.js index eacc336ea780..9b5f2080491f 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -7,6 +7,7 @@ var plugins = require('../plugins'); var topics = require('../topics'); var groups = require('../groups'); var privileges = require('../privileges'); +var cache = require('../cache'); module.exports = function (Categories) { Categories.purge = function (cid, uid, callback) { @@ -94,7 +95,18 @@ module.exports = function (Categories) { ], next); }, next); }, - ], next); + ], function (err) { + if (err) { + return next(err); + } + cache.del([ + 'categories:cid', + 'cid:0:children', + 'cid:' + results.parentCid + ':children', + 'cid:' + cid + ':children', + ]); + next(); + }); }, ], function (err) { callback(err); diff --git a/src/categories/index.js b/src/categories/index.js index f8d0d2390fcd..8f5418c9b89b 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -9,6 +9,7 @@ var user = require('../user'); var Groups = require('../groups'); var plugins = require('../plugins'); var privileges = require('../privileges'); +const cache = require('../cache'); var Categories = module.exports; @@ -82,10 +83,25 @@ Categories.isIgnored = function (cids, uid, callback) { db.isSortedSetMembers('uid:' + uid + ':ignored:cids', cids, callback); }; +Categories.getAllCidsFromSet = function (key, callback) { + const cids = cache.get(key); + if (cids) { + return setImmediate(callback, null, cids.slice()); + } + + db.getSortedSetRange(key, 0, -1, function (err, cids) { + if (err) { + return callback(err); + } + cache.set(key, cids); + callback(null, cids.slice()); + }); +}; + Categories.getAllCategories = function (uid, callback) { async.waterfall([ function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + Categories.getAllCids(next); }, function (cids, next) { Categories.getCategories(cids, uid, next); @@ -96,7 +112,7 @@ Categories.getAllCategories = function (uid, callback) { Categories.getCidsByPrivilege = function (set, uid, privilege, callback) { async.waterfall([ function (next) { - db.getSortedSetRange(set, 0, -1, next); + Categories.getAllCidsFromSet(set, next); }, function (cids, next) { privileges.categories.filterCids(privilege, cids, uid, next); @@ -268,7 +284,7 @@ function getChildrenTree(category, uid, callback) { } Categories.getChildrenCids = function (rootCid, callback) { - var allCids = []; + let allCids = []; function recursive(keys, callback) { db.getSortedSetRange(keys, 0, -1, function (err, childrenCids) { if (err) { @@ -283,9 +299,19 @@ Categories.getChildrenCids = function (rootCid, callback) { recursive(keys, callback); }); } + const key = 'cid:' + rootCid + ':children'; + const childrenCids = cache.get(key); + if (childrenCids) { + return setImmediate(callback, null, childrenCids.slice()); + } - recursive('cid:' + rootCid + ':children', function (err) { - callback(err, _.uniq(allCids)); + recursive(key, function (err) { + if (err) { + return callback(err); + } + allCids = _.uniq(allCids); + cache.set(key, allCids); + callback(null, allCids.slice()); }); }; diff --git a/src/categories/update.js b/src/categories/update.js index c3d89ce67d9d..b876cd22d643 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -1,4 +1,3 @@ - 'use strict'; var async = require('async'); @@ -8,6 +7,7 @@ var meta = require('../meta'); var utils = require('../utils'); var translator = require('../translator'); var plugins = require('../plugins'); +var cache = require('../cache'); module.exports = function (Categories) { Categories.update = function (modified, callback) { @@ -112,6 +112,10 @@ module.exports = function (Categories) { function (next) { db.setObjectField('category:' + cid, 'parentCid', newParent, next); }, + function (next) { + cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); + next(); + }, ], next); }, ], function (err) { @@ -149,6 +153,10 @@ module.exports = function (Categories) { function (next) { db.sortedSetAdd('cid:' + parentCid + ':children', order, cid, next); }, + function (next) { + cache.del(['categories:cid', 'cid:' + parentCid + ':children']); + next(); + }, ], next); }, ], function (err) { diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js index 51c4fbb51b82..2a85e6d07b88 100644 --- a/src/controllers/admin/cache.js +++ b/src/controllers/admin/cache.js @@ -8,6 +8,7 @@ cacheController.get = function (req, res) { var postCache = require('../../posts/cache'); var groupCache = require('../../groups').cache; var objectCache = require('../../database').objectCache; + var localCache = require('../../cache'); var avgPostSize = 0; var percentFull = 0; @@ -32,11 +33,20 @@ cacheController.get = function (req, res) { max: groupCache.max, itemCount: groupCache.itemCount, percentFull: ((groupCache.length / groupCache.max) * 100).toFixed(2), - dump: req.query.debug ? JSON.stringify(groupCache.dump(), null, 4) : false, hits: utils.addCommas(String(groupCache.hits)), misses: utils.addCommas(String(groupCache.misses)), hitRatio: (groupCache.hits / (groupCache.hits + groupCache.misses)).toFixed(4), }, + 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)).toFixed(4), + }, }; if (objectCache) { @@ -45,7 +55,6 @@ cacheController.get = function (req, res) { max: objectCache.max, itemCount: objectCache.itemCount, percentFull: ((objectCache.length / objectCache.max) * 100).toFixed(2), - dump: req.query.debug ? JSON.stringify(objectCache.dump(), null, 4) : false, hits: utils.addCommas(String(objectCache.hits)), misses: utils.addCommas(String(objectCache.misses)), hitRatio: (objectCache.hits / (objectCache.hits + objectCache.misses)).toFixed(4), diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js index 45fabeb2d480..16678ba08674 100644 --- a/src/controllers/admin/homepage.js +++ b/src/controllers/admin/homepage.js @@ -2,7 +2,6 @@ var async = require('async'); -var db = require('../../database'); var categories = require('../../categories'); var privileges = require('../../privileges'); var plugins = require('../../plugins'); @@ -12,7 +11,7 @@ var homePageController = module.exports; homePageController.get = function (req, res, next) { async.waterfall([ function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, function (cids, next) { privileges.categories.filterCids('find', cids, 0, next); diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js index 93aee0e7e346..e4aa4a400bb6 100644 --- a/src/controllers/admin/privileges.js +++ b/src/controllers/admin/privileges.js @@ -2,7 +2,6 @@ var async = require('async'); -var db = require('../../database'); var categories = require('../../categories'); var privileges = require('../../privileges'); @@ -23,7 +22,7 @@ privilegesController.get = function (req, res, callback) { allCategories: function (next) { async.waterfall([ function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, function (cids, next) { categories.getCategories(cids, req.uid, next); diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index dfd205c398c1..3d9f0346fdd5 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -2,7 +2,6 @@ var async = require('async'); -var db = require('../../database'); var groups = require('../../groups'); var categories = require('../../categories'); var privileges = require('../../privileges'); @@ -21,7 +20,7 @@ Categories.create = function (socket, data, callback) { Categories.getAll = function (socket, data, callback) { async.waterfall([ - async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + async.apply(categories.getAllCidsFromSet, 'categories:cid'), async.apply(categories.getCategoriesData), function (categories, next) { // Hook changes, there is no req, and res diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 4b009ec6d53b..06cfbfecd77c 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,7 +1,7 @@ 'use strict'; var async = require('async'); -var db = require('../database'); + var categories = require('../categories'); var privileges = require('../privileges'); var user = require('../user'); @@ -21,7 +21,7 @@ SocketCategories.get = function (socket, data, callback) { isAdmin: async.apply(user.isAdministrator, socket.uid), categories: function (next) { async.waterfall([ - async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + async.apply(categories.getAllCidsFromSet, 'categories:cid'), async.apply(categories.getCategoriesData), ], next); }, @@ -139,7 +139,7 @@ SocketCategories.getMoveCategories = function (socket, data, callback) { categories: function (next) { async.waterfall([ function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, function (cids, next) { categories.getCategories(cids, socket.uid, next); @@ -183,7 +183,7 @@ function ignoreOrWatch(fn, socket, cid, callback) { user.isAdminOrGlobalModOrSelf(socket.uid, targetUid, next); }, function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, function (cids, next) { categories.getCategoriesFields(cids, ['cid', 'parentCid'], next); diff --git a/src/user/categories.js b/src/user/categories.js index 541256e464c0..8a825e2a6154 100644 --- a/src/user/categories.js +++ b/src/user/categories.js @@ -14,9 +14,6 @@ module.exports = function (User) { }; User.getWatchedCategories = function (uid, callback) { - if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, []); - } async.waterfall([ function (next) { async.parallel({ @@ -24,16 +21,13 @@ module.exports = function (User) { User.getIgnoredCategories(uid, next); }, all: function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, }, next); }, function (results, next) { const ignored = new Set(results.ignored); - - var watched = results.all.filter(function (cid) { - return cid && !ignored.has(String(cid)); - }); + const watched = results.all.filter(cid => cid && !ignored.has(String(cid))); next(null, watched); }, ], callback); diff --git a/src/user/index.js b/src/user/index.js index 50529460b068..d8c19ed70534 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -7,6 +7,7 @@ var groups = require('../groups'); var plugins = require('../plugins'); var db = require('../database'); var privileges = require('../privileges'); +var categories = require('../categories'); var meta = require('../meta'); var User = module.exports; @@ -288,7 +289,7 @@ User.getAdminsandGlobalModsandModerators = function (callback) { User.getModeratorUids = function (callback) { async.waterfall([ - async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + async.apply(categories.getAllCidsFromSet, 'categories:cid'), function (cids, next) { var groupNames = cids.reduce(function (memo, cid) { memo.push('cid:' + cid + ':privileges:moderate'); @@ -321,19 +322,20 @@ User.getModeratorUids = function (callback) { }; User.getModeratedCids = function (uid, callback) { + if (parseInt(uid, 10) <= 0) { + return setImmediate(callback, null, []); + } var cids; async.waterfall([ function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); + categories.getAllCidsFromSet('categories:cid', next); }, function (_cids, next) { cids = _cids; User.isModerator(uid, cids, next); }, function (isMods, next) { - cids = cids.filter(function (cid, index) { - return cid && isMods[index]; - }); + cids = cids.filter((cid, index) => cid && isMods[index]); next(null, cids); }, ], callback); diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index 93824f43f24f..c89e25feb6f6 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -4,7 +4,6 @@
[[admin/advanced/cache:post-cache]]
-
{postCache.itemCount}
@@ -30,6 +29,7 @@
+
Object Cache
@@ -45,10 +45,6 @@ {objectCache.hits}
{objectCache.misses}
{objectCache.hitRatio}
- - -
{objectCache.dump}
-
@@ -69,10 +65,29 @@ {groupCache.hits}
{groupCache.misses}
{groupCache.hitRatio}
+ + + +
+
Local Cache
+
+ +
+ {localCache.length} / {localCache.max}
+ +
+
+ [[admin/advanced/cache:percent-full, {localCache.percentFull}]] +
+
+ + {localCache.hits}
+ {localCache.misses}
+ {localCache.hitRatio}
- -
{groupCache.dump}
- + +
{localCache.dump}
+
diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 6caae028214a..fc293529e5ef 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -175,6 +175,8 @@ function setupMockDefaults(callback) { groups.resetCache(); var postCache = require('../../src/posts/cache'); postCache.reset(); + var localCache = require('../../src/cache'); + localCache.reset(); next(); }, function (next) {