diff --git a/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js b/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js index e20120ad19..a44d34e8ea 100644 --- a/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js +++ b/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js @@ -1,6 +1,6 @@ +const crypto = require('crypto'); const BaseCacheAdapter = require('@tryghost/adapter-base-cache'); const logging = require('@tryghost/logging'); -const metrics = require('@tryghost/metrics'); const debug = require('@tryghost/debug')('redis-cache'); const cacheManager = require('cache-manager'); const redisStoreFactory = require('./redis-store-factory'); @@ -61,22 +61,41 @@ class AdapterCacheRedis extends BaseCacheAdapter { this.ttl = config.ttl; this.refreshAheadFactor = config.refreshAheadFactor || 0; this.currentlyExecutingBackgroundRefreshes = new Set(); - this.keyPrefix = config.keyPrefix; - this._keysPattern = config.keyPrefix ? `${config.keyPrefix}*` : ''; + this._keyPrefix = config.keyPrefix || ''; this.redisClient = this.cache.store.getClient(); this.redisClient.on('error', this.handleRedisError); } + async prefixHash() { + const currentPrefixHash = await this.cache.get(this._keyPrefix + 'prefix_hash'); + if (currentPrefixHash) { + return currentPrefixHash; + } + const newPrefixHash = await this.cyclePrefixHash(); + return newPrefixHash; + } + + async keyPrefix() { + const prefixHash = await this.prefixHash(); + return this._keyPrefix + prefixHash; + } + + async keysPattern() { + const keyPrefix = await this.keyPrefix(); + return keyPrefix + '*'; + } + handleRedisError(error) { logging.error(error); } - #getPrimaryRedisNode() { + async #getPrimaryRedisNode() { debug('getPrimaryRedisNode'); if (this.redisClient.constructor.name !== 'Cluster') { return this.redisClient; } - const slot = calculateSlot(this.keyPrefix); + const keyPrefix = await this.keyPrefix(); + const slot = calculateSlot(keyPrefix); const [ip, port] = this.redisClient.slots[slot][0].split(':'); for (const node of this.redisClient.nodes()) { if (node.options.host === ip && node.options.port === parseInt(port)) { @@ -86,10 +105,11 @@ class AdapterCacheRedis extends BaseCacheAdapter { return null; } - #scanNodeForKeys(node) { - debug(`scanNodeForKeys matching ${this._keysPattern}`); + async #scanNodeForKeys(node) { + const keysPattern = await this.keysPattern(); + debug(`scanNodeForKeys matching ${keysPattern}`); return new Promise((resolve, reject) => { - const stream = node.scanStream({match: this._keysPattern, count: 100}); + const stream = node.scanStream({match: keysPattern, count: 100}); let keys = []; stream.on('data', (resultKeys) => { keys = keys.concat(resultKeys); @@ -105,7 +125,7 @@ class AdapterCacheRedis extends BaseCacheAdapter { async #getKeys() { debug('#getKeys'); - const primaryNode = this.#getPrimaryRedisNode(); + const primaryNode = await this.#getPrimaryRedisNode(); if (primaryNode === null) { return []; } @@ -117,11 +137,12 @@ class AdapterCacheRedis extends BaseCacheAdapter { * the cache-manager package. Might be a good contribution to make * in the package itself (https://github.com/node-cache-manager/node-cache-manager/issues/158) * @param {string} key - * @returns {string} + * @returns {Promise} */ - _buildKey(key) { - if (this.keyPrefix) { - return `${this.keyPrefix}${key}`; + async _buildKey(key) { + const keyPrefix = await this.keyPrefix(); + if (keyPrefix) { + return `${keyPrefix}${key}`; } return key; @@ -130,10 +151,11 @@ class AdapterCacheRedis extends BaseCacheAdapter { /** * This is a method to remove the key prefix from any raw key returned from redis. * @param {string} key - * @returns {string} + * @returns {Promise} */ - _removeKeyPrefix(key) { - return key.slice(this.keyPrefix.length); + async _removeKeyPrefix(key) { + const keyPrefix = await this.keyPrefix(); + return key.slice(keyPrefix.length); } /** @@ -166,7 +188,7 @@ class AdapterCacheRedis extends BaseCacheAdapter { * @param {() => Promise} [fetchData] An optional function to fetch the data, which will be used in the case of a cache MISS or a background refresh */ async get(key, fetchData) { - const internalKey = this._buildKey(key); + const internalKey = await this._buildKey(key); try { const result = await this.cache.get(internalKey); debug(`get ${internalKey}: Cache ${result ? 'HIT' : 'MISS'}`); @@ -210,7 +232,7 @@ class AdapterCacheRedis extends BaseCacheAdapter { * @param {*} value */ async set(key, value) { - const internalKey = this._buildKey(key); + const internalKey = await this._buildKey(key); debug('set', internalKey); try { return await this.cache.set(internalKey, value); @@ -219,28 +241,18 @@ class AdapterCacheRedis extends BaseCacheAdapter { } } + async cyclePrefixHash() { + const value = crypto.randomBytes(12).toString('hex'); + await this.cache.set(this._keyPrefix + 'prefix_hash', value, {ttl: 0}); + return value; + } + /** - * Reset the cache by deleting everything from redis + * Reset the cache by refreshing the current prefix hash */ async reset() { debug('reset'); - try { - const t0 = performance.now(); - logging.debug(`[RedisAdapter] Clearing cache: scanning for keys matching ${this._keysPattern}`); - const keys = await this.#getKeys(); - logging.debug(`[RedisAdapter] Clearing cache: found ${keys.length} keys matching ${this._keysPattern} in ${(performance.now() - t0).toFixed(1)}ms`); - metrics.metric('cache-reset-scan', (performance.now() - t0).toFixed(1)); - const t1 = performance.now(); - for (const key of keys) { - await this.cache.del(key); - } - logging.debug(`[RedisAdapter] Clearing cache: deleted ${keys.length} keys matching ${this._keysPattern} in ${(performance.now() - t1).toFixed(1)}ms`); - metrics.metric('cache-reset-delete', (performance.now() - t1).toFixed(1)); - metrics.metric('cache-reset', (performance.now() - t0).toFixed(1)); - metrics.metric('cache-reset-key-count', keys.length); - } catch (err) { - logging.error(err); - } + await this.cyclePrefixHash(); } /** @@ -249,9 +261,9 @@ class AdapterCacheRedis extends BaseCacheAdapter { */ async keys() { try { - return (await this.#getKeys()).map((key) => { + return Promise.all((await this.#getKeys()).map((key) => { return this._removeKeyPrefix(key); - }); + })); } catch (err) { logging.error(err); } diff --git a/ghost/adapter-cache-redis/test/adapter-cache-redis.test.js b/ghost/adapter-cache-redis/test/adapter-cache-redis.test.js index 167498f594..545499fe67 100644 --- a/ghost/adapter-cache-redis/test/adapter-cache-redis.test.js +++ b/ghost/adapter-cache-redis/test/adapter-cache-redis.test.js @@ -64,12 +64,17 @@ describe('Adapter Cache Redis', function () { let cachedValue = null; const redisCacheInstanceStub = { get: function (key) { - assert(key === KEY); - return cachedValue; + if (key === 'prefix_hash') { + return 'prefix_hash'; + } + if (key === 'prefix_hash' + KEY) { + return cachedValue; + } }, set: function (key, value) { - assert(key === KEY); - cachedValue = value; + if (key === 'prefix_hash' + KEY) { + cachedValue = value; + } }, store: { getClient: sinon.stub().returns({ @@ -105,16 +110,22 @@ describe('Adapter Cache Redis', function () { const redisCacheInstanceStub = { get: function (key) { - assert(key === KEY); - return cachedValue; + if (key === 'prefix_hash') { + return 'prefix_hash'; + } + if (key === 'prefix_hash' + KEY) { + return cachedValue; + } }, set: function (key, value) { - assert(key === KEY); - cachedValue = value; + if (key === 'prefix_hash' + KEY) { + cachedValue = value; + } }, ttl: function (key) { - assert(key === KEY); - return remainingTTL; + if (key === 'prefix_hash' + KEY) { + return remainingTTL; + } }, store: { getClient: sinon.stub().returns({ @@ -185,6 +196,11 @@ describe('Adapter Cache Redis', function () { describe('set', function () { it('can set a value in the cache', async function () { const redisCacheInstanceStub = { + get: function (key) { + if (key === 'prefix_hash') { + return 'prefix_hash'; + } + }, set: sinon.stub().resolvesArg(1), store: { getClient: sinon.stub().returns({ @@ -199,11 +215,16 @@ describe('Adapter Cache Redis', function () { const value = await cache.set('key-here', 'new value'); assert.equal(value, 'new value'); - assert.equal(redisCacheInstanceStub.set.args[0][0], 'key-here'); + assert.equal(redisCacheInstanceStub.set.args[0][0], 'prefix_hashkey-here'); }); it('sets a key based on keyPrefix', async function () { const redisCacheInstanceStub = { + get: function (key) { + if (key === 'testing-prefix:prefix_hash') { + return 'prefix_hash'; + } + }, set: sinon.stub().resolvesArg(1), store: { getClient: sinon.stub().returns({ @@ -219,14 +240,15 @@ describe('Adapter Cache Redis', function () { const value = await cache.set('key-here', 'new value'); assert.equal(value, 'new value'); - assert.equal(redisCacheInstanceStub.set.args[0][0], 'testing-prefix:key-here'); + assert.equal(redisCacheInstanceStub.set.args[0][0], 'testing-prefix:prefix_hashkey-here'); }); }); describe('reset', function () { - it('catches an error when thrown during the reset', async function () { + it('Updates the prefix_hash cache input with 0 ttl', async function () { const redisCacheInstanceStub = { get: sinon.stub().resolves('value from cache'), + set: sinon.stub().resolvesArg(1), store: { getClient: sinon.stub().returns({ on: sinon.stub() @@ -239,7 +261,8 @@ describe('Adapter Cache Redis', function () { await cache.reset(); - assert.ok(logging.error.calledOnce, 'error was logged'); + assert.equal(redisCacheInstanceStub.set.args[0][0], 'prefix_hash'); + assert.deepEqual(redisCacheInstanceStub.set.args[0][2], {ttl: 0}); }); }); });