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
This commit is contained in:
Naz 2023-09-06 20:05:52 +08:00 committed by naz
parent b91714f80e
commit cc48ead945
3 changed files with 62 additions and 4 deletions

View File

@ -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
});
}

View File

@ -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;

View File

@ -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');
});
});