From 9b753d6d57b850ef5ebc50e5a3dd7b2cbe4d5a27 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 10 Aug 2022 15:26:07 -0400 Subject: [PATCH] TTL Cache (#10816) * refactor: move src/cacheCreate.js to src/cache/lru.js * fix: call new library location for lru cache creator * feat: add ttl cache * fix: update upload throttler to use ttl cache instead of lru cache * chore: add missing dependency * fix: avoid pubsub conflicts * fix: use get instead of peek, which is not available in ttl-cache --- install/package.json | 1 + src/analytics.js | 2 +- src/cache.js | 2 +- src/cache/lru.js | 143 ++++++++++++++++++++++++++++++++++++++ src/cache/ttl.js | 94 +++++++++++++++++++++++++ src/cacheCreate.js | 142 +------------------------------------ src/database/cache.js | 2 +- src/groups/cache.js | 2 +- src/middleware/index.js | 2 +- src/middleware/uploads.js | 4 +- src/posts/cache.js | 2 +- src/user/blocks.js | 2 +- 12 files changed, 248 insertions(+), 150 deletions(-) create mode 100644 src/cache/lru.js create mode 100644 src/cache/ttl.js diff --git a/install/package.json b/install/package.json index 855ba36cde..94c22d2872 100644 --- a/install/package.json +++ b/install/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@adactive/bootstrap-tagsinput": "0.8.2", + "@isaacs/ttlcache": "^1.2.0", "ace-builds": "1.8.1", "archiver": "5.3.1", "async": "3.2.4", diff --git a/src/analytics.js b/src/analytics.js index f9e14b4caf..03be429637 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -14,7 +14,7 @@ const utils = require('./utils'); const plugins = require('./plugins'); const meta = require('./meta'); const pubsub = require('./pubsub'); -const cacheCreate = require('./cacheCreate'); +const cacheCreate = require('./cache/lru'); const Analytics = module.exports; diff --git a/src/cache.js b/src/cache.js index 6dcc440536..996faf9237 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,6 +1,6 @@ 'use strict'; -const cacheCreate = require('./cacheCreate'); +const cacheCreate = require('./cache/lru'); module.exports = cacheCreate({ name: 'local', diff --git a/src/cache/lru.js b/src/cache/lru.js new file mode 100644 index 0000000000..d0a92c46d5 --- /dev/null +++ b/src/cache/lru.js @@ -0,0 +1,143 @@ +'use strict'; + +module.exports = function (opts) { + const LRU = require('lru-cache'); + const pubsub = require('../pubsub'); + + // lru-cache@7 deprecations + const winston = require('winston'); + const chalk = require('chalk'); + + // sometimes we kept passing in `length` with no corresponding `maxSize`. + // This is now enforced in v7; drop superfluous property + if (opts.hasOwnProperty('length') && !opts.hasOwnProperty('maxSize')) { + winston.warn(`[cache/init(${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} ${chalk.yellow('length')} was passed in without a corresponding ${chalk.yellow('maxSize')}. Both are now required as of lru-cache@7.0.0.`); + delete opts.length; + } + + const deprecations = new Map([ + ['stale', 'allowStale'], + ['maxAge', 'ttl'], + ['length', 'sizeCalculation'], + ]); + deprecations.forEach((newProp, oldProp) => { + if (opts.hasOwnProperty(oldProp) && !opts.hasOwnProperty(newProp)) { + winston.warn(`[cache/init(${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} The option ${chalk.yellow(oldProp)} has been deprecated as of lru-cache@7.0.0. Please change this to ${chalk.yellow(newProp)} instead.`); + opts[newProp] = opts[oldProp]; + delete opts[oldProp]; + } + }); + + const lruCache = new LRU(opts); + + const cache = {}; + cache.name = opts.name; + cache.hits = 0; + cache.misses = 0; + cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true; + const cacheSet = lruCache.set; + + // backwards compatibility + const propertyMap = new Map([ + ['length', 'calculatedSize'], + ['max', 'max'], + ['maxSize', 'maxSize'], + ['itemCount', 'size'], + ]); + propertyMap.forEach((lruProp, cacheProp) => { + Object.defineProperty(cache, cacheProp, { + get: function () { + return lruCache[lruProp]; + }, + configurable: true, + enumerable: true, + }); + }); + + cache.set = function (key, value, ttl) { + if (!cache.enabled) { + return; + } + const opts = {}; + if (ttl) { + opts.ttl = ttl; + } + cacheSet.apply(lruCache, [key, value, opts]); + }; + + cache.get = function (key) { + if (!cache.enabled) { + return undefined; + } + const data = lruCache.get(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}:lruCache:del`, keys); + keys.forEach(key => lruCache.delete(key)); + }; + cache.delete = cache.del; + + cache.reset = function () { + pubsub.publish(`${cache.name}:lruCache:reset`); + localReset(); + }; + cache.clear = cache.reset; + + function localReset() { + lruCache.clear(); + cache.hits = 0; + cache.misses = 0; + } + + pubsub.on(`${cache.name}:lruCache:reset`, () => { + localReset(); + }); + + pubsub.on(`${cache.name}:lruCache:del`, (keys) => { + if (Array.isArray(keys)) { + keys.forEach(key => lruCache.delete(key)); + } + }); + + cache.getUnCachedKeys = function (keys, cachedData) { + if (!cache.enabled) { + return keys; + } + let data; + let isCached; + const unCachedKeys = keys.filter((key) => { + data = cache.get(key); + isCached = data !== undefined; + if (isCached) { + cachedData[key] = data; + } + return !isCached; + }); + + const hits = keys.length - unCachedKeys.length; + const misses = keys.length - hits; + cache.hits += hits; + cache.misses += misses; + return unCachedKeys; + }; + + cache.dump = function () { + return lruCache.dump(); + }; + + cache.peek = function (key) { + return lruCache.peek(key); + }; + + return cache; +}; diff --git a/src/cache/ttl.js b/src/cache/ttl.js new file mode 100644 index 0000000000..3b9d3fee77 --- /dev/null +++ b/src/cache/ttl.js @@ -0,0 +1,94 @@ +'use strict'; + +module.exports = function (opts) { + const TTLCache = require('@isaacs/ttlcache'); + const pubsub = require('../pubsub'); + + const ttlCache = new TTLCache(opts); + + const cache = {}; + cache.name = opts.name; + cache.hits = 0; + cache.misses = 0; + cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true; + const cacheSet = ttlCache.set; + + cache.set = function (key, value, ttl) { + if (!cache.enabled) { + return; + } + const opts = {}; + if (ttl) { + opts.ttl = ttl; + } + cacheSet.apply(ttlCache, [key, value, opts]); + }; + + cache.get = function (key) { + if (!cache.enabled) { + return undefined; + } + const data = ttlCache.get(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}:ttlCache:del`, keys); + keys.forEach(key => ttlCache.delete(key)); + }; + cache.delete = cache.del; + + cache.reset = function () { + pubsub.publish(`${cache.name}:ttlCache:reset`); + localReset(); + }; + cache.clear = cache.reset; + + function localReset() { + ttlCache.clear(); + cache.hits = 0; + cache.misses = 0; + } + + pubsub.on(`${cache.name}:ttlCache:reset`, () => { + localReset(); + }); + + pubsub.on(`${cache.name}:ttlCache:del`, (keys) => { + if (Array.isArray(keys)) { + keys.forEach(key => ttlCache.delete(key)); + } + }); + + cache.getUnCachedKeys = function (keys, cachedData) { + if (!cache.enabled) { + return keys; + } + let data; + let isCached; + const unCachedKeys = keys.filter((key) => { + data = cache.get(key); + isCached = data !== undefined; + if (isCached) { + cachedData[key] = data; + } + return !isCached; + }); + + const hits = keys.length - unCachedKeys.length; + const misses = keys.length - hits; + cache.hits += hits; + cache.misses += misses; + return unCachedKeys; + }; + + return cache; +}; diff --git a/src/cacheCreate.js b/src/cacheCreate.js index deaef9bba4..14a5a7a79b 100644 --- a/src/cacheCreate.js +++ b/src/cacheCreate.js @@ -1,143 +1,3 @@ 'use strict'; -module.exports = function (opts) { - const LRU = require('lru-cache'); - const pubsub = require('./pubsub'); - - // lru-cache@7 deprecations - const winston = require('winston'); - const chalk = require('chalk'); - - // sometimes we kept passing in `length` with no corresponding `maxSize`. - // This is now enforced in v7; drop superfluous property - if (opts.hasOwnProperty('length') && !opts.hasOwnProperty('maxSize')) { - winston.warn(`[cache/init(${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} ${chalk.yellow('length')} was passed in without a corresponding ${chalk.yellow('maxSize')}. Both are now required as of lru-cache@7.0.0.`); - delete opts.length; - } - - const deprecations = new Map([ - ['stale', 'allowStale'], - ['maxAge', 'ttl'], - ['length', 'sizeCalculation'], - ]); - deprecations.forEach((newProp, oldProp) => { - if (opts.hasOwnProperty(oldProp) && !opts.hasOwnProperty(newProp)) { - winston.warn(`[cache/init (${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} The option ${chalk.yellow(oldProp)} has been deprecated as of lru-cache@7.0.0. Please change this to ${chalk.yellow(newProp)} instead.`); - opts[newProp] = opts[oldProp]; - delete opts[oldProp]; - } - }); - - const lruCache = new LRU(opts); - - const cache = {}; - cache.name = opts.name; - cache.hits = 0; - cache.misses = 0; - cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true; - const cacheSet = lruCache.set; - - // backwards compatibility - const propertyMap = new Map([ - ['length', 'calculatedSize'], - ['max', 'max'], - ['maxSize', 'maxSize'], - ['itemCount', 'size'], - ]); - propertyMap.forEach((lruProp, cacheProp) => { - Object.defineProperty(cache, cacheProp, { - get: function () { - return lruCache[lruProp]; - }, - configurable: true, - enumerable: true, - }); - }); - - cache.set = function (key, value, ttl) { - if (!cache.enabled) { - return; - } - const opts = {}; - if (ttl) { - opts.ttl = ttl; - } - cacheSet.apply(lruCache, [key, value, opts]); - }; - - cache.get = function (key) { - if (!cache.enabled) { - return undefined; - } - const data = lruCache.get(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 => lruCache.delete(key)); - }; - cache.delete = cache.del; - - cache.reset = function () { - pubsub.publish(`${cache.name}:cache:reset`); - localReset(); - }; - cache.clear = cache.reset; - - function localReset() { - lruCache.clear(); - cache.hits = 0; - cache.misses = 0; - } - - pubsub.on(`${cache.name}:cache:reset`, () => { - localReset(); - }); - - pubsub.on(`${cache.name}:cache:del`, (keys) => { - if (Array.isArray(keys)) { - keys.forEach(key => lruCache.delete(key)); - } - }); - - cache.getUnCachedKeys = function (keys, cachedData) { - if (!cache.enabled) { - return keys; - } - let data; - let isCached; - const unCachedKeys = keys.filter((key) => { - data = cache.get(key); - isCached = data !== undefined; - if (isCached) { - cachedData[key] = data; - } - return !isCached; - }); - - const hits = keys.length - unCachedKeys.length; - const misses = keys.length - hits; - cache.hits += hits; - cache.misses += misses; - return unCachedKeys; - }; - - cache.dump = function () { - return lruCache.dump(); - }; - - cache.peek = function (key) { - return lruCache.peek(key); - }; - - return cache; -}; +module.exports = require('./cache/lru'); diff --git a/src/database/cache.js b/src/database/cache.js index 999c860363..07974e9f3b 100644 --- a/src/database/cache.js +++ b/src/database/cache.js @@ -1,7 +1,7 @@ 'use strict'; module.exports.create = function (name) { - const cacheCreate = require('../cacheCreate'); + const cacheCreate = require('../cache/lru'); return cacheCreate({ name: `${name}-object`, max: 40000, diff --git a/src/groups/cache.js b/src/groups/cache.js index b83e3e5928..ede405dcee 100644 --- a/src/groups/cache.js +++ b/src/groups/cache.js @@ -1,6 +1,6 @@ 'use strict'; -const cacheCreate = require('../cacheCreate'); +const cacheCreate = require('../cache/lru'); module.exports = function (Groups) { Groups.cache = cacheCreate({ diff --git a/src/middleware/index.js b/src/middleware/index.js index 6930021aee..d0d3ed346f 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -14,7 +14,7 @@ const user = require('../user'); const groups = require('../groups'); const analytics = require('../analytics'); const privileges = require('../privileges'); -const cacheCreate = require('../cacheCreate'); +const cacheCreate = require('../cache/lru'); const helpers = require('./helpers'); const controllers = { diff --git a/src/middleware/uploads.js b/src/middleware/uploads.js index beb36c03e0..5b1e9c392f 100644 --- a/src/middleware/uploads.js +++ b/src/middleware/uploads.js @@ -1,6 +1,6 @@ 'use strict'; -const cacheCreate = require('../cacheCreate'); +const cacheCreate = require('../cache/ttl'); const meta = require('../meta'); const helpers = require('./helpers'); const user = require('../user'); @@ -19,7 +19,7 @@ exports.ratelimit = helpers.try(async (req, res, next) => { return next(); } - const count = (cache.peek(`${req.ip}:uploaded_file_count`) || 0) + req.files.files.length; + const count = (cache.get(`${req.ip}:uploaded_file_count`) || 0) + req.files.files.length; if (count > meta.config.uploadRateLimitThreshold) { return next(new Error(['[[error:upload-ratelimit-reached]]'])); } diff --git a/src/posts/cache.js b/src/posts/cache.js index 246074697c..888fba6424 100644 --- a/src/posts/cache.js +++ b/src/posts/cache.js @@ -1,6 +1,6 @@ 'use strict'; -const cacheCreate = require('../cacheCreate'); +const cacheCreate = require('../cache/lru'); const meta = require('../meta'); module.exports = cacheCreate({ diff --git a/src/user/blocks.js b/src/user/blocks.js index f71589911c..19ad2c0766 100644 --- a/src/user/blocks.js +++ b/src/user/blocks.js @@ -2,7 +2,7 @@ const db = require('../database'); const plugins = require('../plugins'); -const cacheCreate = require('../cacheCreate'); +const cacheCreate = require('../cache/lru'); module.exports = function (User) { User.blocks = {