From cc48ead9455d3fc0aee4e178989df54119610dc5 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 6 Sep 2023 20:05:52 +0800 Subject: [PATCH] Added option to share redis connections across caches closes https://github.com/TryGhost/Arch/issues/85 - Added a cache configuration option to signal "reuse of redis connection" for Redis cache adapter. The connection reuse it turned on by default to be shared between caches. They rely on unique "keyPrefix" structure, so there is no collision side-effects when reusing same Redis Store. - The Redis connection options like "ttl" are shared with the first connection that's crated. So if there's a need to have unique configuration, a separate connection has to be created by passing `"reuseConnection": false` parameter --- .../lib/AdapterCacheRedis.js | 14 ++++++--- .../lib/redis-store-factory.js | 22 ++++++++++++++ .../test/redis-store-factory.test.js | 30 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 ghost/adapter-cache-redis/lib/redis-store-factory.js create mode 100644 ghost/adapter-cache-redis/test/redis-store-factory.test.js diff --git a/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js b/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js index f943fcdb2c..93644da5b6 100644 --- a/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js +++ b/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js @@ -1,20 +1,21 @@ const BaseCacheAdapter = require('@tryghost/adapter-base-cache'); const logging = require('@tryghost/logging'); const cacheManager = require('cache-manager'); -const redisStore = require('cache-manager-ioredis'); +const redisStoreFactory = require('./redis-store-factory'); const calculateSlot = require('cluster-key-slot'); class AdapterCacheRedis extends BaseCacheAdapter { /** * * @param {Object} config - * @param {Object} [config.cache] - caching instance compatible with cache-manager with redis store + * @param {Object} [config.cache] - caching instance compatible with cache-manager's redis store * @param {String} [config.host] - redis host used in case no cache instance provided * @param {Number} [config.port] - redis port used in case no cache instance provided * @param {String} [config.password] - redis password used in case no cache instance provided * @param {Object} [config.clusterConfig] - redis cluster config used in case no cache instance provided * @param {Number} [config.ttl] - default cached value Time To Live (expiration) in *seconds* * @param {String} [config.keyPrefix] - prefix to use when building a unique cache key, e.g.: 'some_id:image-sizes:' + * @param {Boolean} [config.reuseConnection] - specifies if the redis store/connection should be reused within the process */ constructor(config) { super(); @@ -33,13 +34,18 @@ class AdapterCacheRedis extends BaseCacheAdapter { config.clusterConfig.options.ttl = config.ttl; } - this.cache = cacheManager.caching({ - store: redisStore, + const storeOptions = { ttl: config.ttl, host: config.host, port: config.port, password: config.password, clusterConfig: config.clusterConfig + }; + const store = redisStoreFactory.getRedisStore(storeOptions, config.reuseConnection); + + this.cache = cacheManager.caching({ + store: store, + ...storeOptions }); } diff --git a/ghost/adapter-cache-redis/lib/redis-store-factory.js b/ghost/adapter-cache-redis/lib/redis-store-factory.js new file mode 100644 index 0000000000..b98b9ee36c --- /dev/null +++ b/ghost/adapter-cache-redis/lib/redis-store-factory.js @@ -0,0 +1,22 @@ +const defaultCacheManager = require('cache-manager-ioredis'); +let redisStoreSingletonInstance; + +/** + * + * @param {object} [storeOptions] options to pass to the Redis store instance + * @param {boolean} [reuseConnection] specifies if the Redis store/connection should be reused within the process +* @param {object} [CacheManager] CacheManager constructor to instantiate, defaults to cache-manager-ioredis + */ +const getRedisStore = (storeOptions, reuseConnection = true, CacheManager = defaultCacheManager) => { + if (storeOptions && reuseConnection) { + if (!redisStoreSingletonInstance) { + redisStoreSingletonInstance = CacheManager.create(storeOptions); + } + + return redisStoreSingletonInstance; + } else { + return CacheManager; + } +}; + +module.exports.getRedisStore = getRedisStore; diff --git a/ghost/adapter-cache-redis/test/redis-store-factory.test.js b/ghost/adapter-cache-redis/test/redis-store-factory.test.js new file mode 100644 index 0000000000..7cf5410882 --- /dev/null +++ b/ghost/adapter-cache-redis/test/redis-store-factory.test.js @@ -0,0 +1,30 @@ +const assert = require('assert/strict'); +const redisStoreFactory = require('../lib/redis-store-factory'); + +class CacheManagerMock { + static create() { + return 'StoreInstance' + new Date().getTime(); + } +} + +describe('Redis Store Factory', function () { + it('returns a cache manager constructor when no extra parameters are provided', function () { + const store = redisStoreFactory.getRedisStore(); + + assert.ok(store); + assert.ok(store.create); + }); + + it('reuses redis store instance', function () { + const store = redisStoreFactory.getRedisStore({}, true, CacheManagerMock); + const storeReused = redisStoreFactory.getRedisStore({}, true, CacheManagerMock); + + assert.equal(store.create, undefined); + + assert.equal(store.startsWith('StoreInstance'), true, 'Should be a value of the create method without a random postfix'); + assert.equal(store, storeReused, 'Should be the same store instance'); + + const uniqueStore = redisStoreFactory.getRedisStore({}, false, CacheManagerMock); + assert.notEqual(uniqueStore, store, 'Should be a different store instances'); + }); +});