diff --git a/package.json b/package.json index 6f2494c833..45869d7eb7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "json-2-csv": "^2.0.22", "less": "^2.0.0", "logrotate-stream": "^0.2.3", - "lru-cache": "4.0.1", + "lru-cache": "4.0.2", "mime": "^1.3.4", "minimist": "^1.1.1", "mkdirp": "~0.5.0", diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js index dce16818f8..dea5f0345b 100644 --- a/src/controllers/admin/cache.js +++ b/src/controllers/admin/cache.js @@ -5,6 +5,7 @@ var cacheController = {}; cacheController.get = function (req, res) { var postCache = require('../../posts/cache'); var groupCache = require('../../groups').cache; + var userSettingsCache = require('../../user').settingsCache; var avgPostSize = 0; var percentFull = 0; @@ -21,6 +22,12 @@ cacheController.get = function (req, res) { percentFull: percentFull, avgPostSize: avgPostSize, }, + userSettingsCache: { + length: userSettingsCache.length, + max: userSettingsCache.max, + itemCount: userSettingsCache.itemCount, + percentFull: ((userSettingsCache.length / userSettingsCache.max) * 100).toFixed(2), + }, groupCache: { length: groupCache.length, max: groupCache.max, diff --git a/src/user/settings.js b/src/user/settings.js index bef20e0087..76da620d32 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -2,83 +2,121 @@ 'use strict'; var async = require('async'); + var meta = require('../meta'); var db = require('../database'); var plugins = require('../plugins'); +var LRU = require('lru-cache'); + +var cache = LRU({ + max: 500, + length: function () { return 1; }, + maxAge: 1000 * 60 * 60, +}); + module.exports = function (User) { + User.settingsCache = cache; + User.getSettings = function (uid, callback) { if (!parseInt(uid, 10)) { return onSettingsLoaded(0, {}, callback); } - db.getObject('user:' + uid + ':settings', function (err, settings) { - if (err) { - return callback(err); - } + var cached = cache.get('user:' + uid + ':settings'); + if (cached) { + return onSettingsLoaded(uid, cached || {}, callback); + } - onSettingsLoaded(uid, settings || {}, callback); - }); + async.waterfall([ + function (next) { + db.getObject('user:' + uid + ':settings', next); + }, + function (settings, next) { + settings = settings || {}; + settings.uid = uid; + cache.set('user:' + uid + ':settings', settings); + onSettingsLoaded(uid, settings || {}, next); + }, + ], callback); }; User.getMultipleUserSettings = function (uids, callback) { + function getFromCache(next) { + var settings = uids.map(function (uid) { + return cache.get('user:' + uid + ':settings') || {}; + }); + async.map(settings, function (setting, next) { + onSettingsLoaded(setting.uid, setting, next); + }, next); + } + + if (!Array.isArray(uids) || !uids.length) { return callback(null, []); } - var keys = uids.map(function (uid) { - return 'user:' + uid + ':settings'; + var nonCachedUids = uids.filter(function (uid) { + return !cache.has('user:' + uid + ':settings'); }); - db.getObjects(keys, function (err, settings) { - if (err) { - return callback(err); - } - - for (var i = 0; i < settings.length; i += 1) { - settings[i] = settings[i] || {}; - settings[i].uid = uids[i]; - } + if (!nonCachedUids.length) { + return getFromCache(callback); + } - async.map(settings, function (setting, next) { - onSettingsLoaded(setting.uid, setting, next); - }, callback); + var keys = nonCachedUids.map(function (uid) { + return 'user:' + uid + ':settings'; }); + + async.waterfall([ + function (next) { + db.getObjects(keys, next); + }, + function (settings, next) { + settings.forEach(function (userSettings, index) { + userSettings = userSettings || {}; + userSettings.uid = nonCachedUids[index]; + cache.set('user:' + userSettings.uid + ':settings', userSettings); + }); + + getFromCache(next); + }, + ], callback); }; function onSettingsLoaded(uid, settings, callback) { - plugins.fireHook('filter:user.getSettings', { uid: uid, settings: settings }, function (err, data) { - if (err) { - return callback(err); - } - - settings = data.settings; - - var defaultTopicsPerPage = parseInt(meta.config.topicsPerPage, 10) || 20; - var defaultPostsPerPage = parseInt(meta.config.postsPerPage, 10) || 20; - - settings.showemail = parseInt(getSetting(settings, 'showemail', 0), 10) === 1; - settings.showfullname = parseInt(getSetting(settings, 'showfullname', 0), 10) === 1; - settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1; - settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off'); - settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; - settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage); - settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage); - settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; - settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); - settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); - settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; - settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1; - settings.sendChatNotifications = parseInt(getSetting(settings, 'sendChatNotifications', 0), 10) === 1; - settings.sendPostNotifications = parseInt(getSetting(settings, 'sendPostNotifications', 0), 10) === 1; - settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; - settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; - settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1; - settings.bootswatchSkin = settings.bootswatchSkin || meta.config.bootswatchSkin || 'default'; - settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; - - callback(null, settings); - }); + async.waterfall([ + function (next) { + plugins.fireHook('filter:user.getSettings', { uid: uid, settings: settings }, next); + }, + function (data, next) { + settings = data.settings; + + var defaultTopicsPerPage = parseInt(meta.config.topicsPerPage, 10) || 20; + var defaultPostsPerPage = parseInt(meta.config.postsPerPage, 10) || 20; + + settings.showemail = parseInt(getSetting(settings, 'showemail', 0), 10) === 1; + settings.showfullname = parseInt(getSetting(settings, 'showfullname', 0), 10) === 1; + settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1; + settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off'); + settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; + settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage); + settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage); + settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; + settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); + settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); + settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; + settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1; + settings.sendChatNotifications = parseInt(getSetting(settings, 'sendChatNotifications', 0), 10) === 1; + settings.sendPostNotifications = parseInt(getSetting(settings, 'sendPostNotifications', 0), 10) === 1; + settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; + settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; + settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1; + settings.bootswatchSkin = settings.bootswatchSkin || meta.config.bootswatchSkin || 'default'; + settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; + next(null, settings); + }, + ], callback); } function getSetting(settings, key, defaultValue) { @@ -138,6 +176,7 @@ module.exports = function (User) { User.updateDigestSetting(uid, data.dailyDigestFreq, next); }, function (next) { + cache.del('user:' + uid + ':settings'); User.getSettings(uid, next); }, ], callback); @@ -160,8 +199,9 @@ module.exports = function (User) { User.setSetting = function (uid, key, value, callback) { if (!parseInt(uid, 10)) { - return callback(); + return setImmediate(callback); } + cache.del('user:' + uid + ':settings'); db.setObjectField('user:' + uid + ':settings', key, value, callback); }; }; diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index 126cba8db0..72854a34b0 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -26,6 +26,24 @@ +
+
User Settings Cache
+
+ +
+ {userSettingsCache.itemCount}
+ +
+ {userSettingsCache.length} / {userSettingsCache.max}
+ +
+
+ [[admin/advanced/cache:percent-full, {userSettingsCache.percentFull}]] +
+
+
+
+
Group Cache