Ghost/ghost/adapter-cache-redis/test/adapter-cache-redis.test.js
Chris Raible d8b629c713
Added an optional timeout parameter to AdapterCacheRedis (#20131)
ref
https://linear.app/tryghost/issue/ENG-902/add-an-optional-timeout-in-the-redis-cache-adapter-in-case-redis

- Added an optional timeout parameter to AdapterCacheRedis, so that the
`get(key)` method will return `null` after the timeout if it hasn't
received a response from Redis
- When load testing the `LinkRedirectRepository` with the Redis cache
enabled on staging, we noticed that for some reason Redis stopped
responding to commands for ~30 seconds.
- The `LinkRedirectRepository` was waiting for the Redis cache to
respond and resulted in a drastic increase in response times for link
redirects
- This change will allow us to set a timeout on the `get(key)` method,
so that if Redis doesn't respond within the timeout, the method will
return `null` as if it were a cache miss.
- Then the `LinkRedirectRepository` will fall back to the database and
return the link redirect from the database instead of waiting
indefinitely for Redis to respond
2024-05-02 20:39:23 -07:00

271 lines
8.8 KiB
JavaScript

const assert = require('assert/strict');
const sinon = require('sinon');
const logging = require('@tryghost/logging');
const RedisCache = require('../index');
describe('Adapter Cache Redis', function () {
beforeEach(function () {
sinon.stub(logging, 'error');
});
afterEach(function () {
sinon.restore();
});
it('can initialize Redis cache instance directly', async function () {
const redisCacheInstanceStub = {
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub
});
assert.ok(cache);
});
it('can initialize with storeConfig', async function () {
const cache = new RedisCache({
username: 'myusername',
storeConfig: {
retryStrategy: false,
lazyConnect: true
},
reuseConnection: false
});
assert.ok(cache);
assert.equal(cache.redisClient.options.username, 'myusername');
assert.equal(cache.redisClient.options.retryStrategy, false);
});
describe('get', function () {
it('can get a value from the cache', async function () {
const redisCacheInstanceStub = {
get: sinon.stub().resolves('value from cache'),
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub
});
const value = await cache.get('key');
assert.equal(value, 'value from cache');
});
it('returns null if getTimeoutMilliseconds is exceeded', async function () {
const redisCacheInstanceStub = {
get: sinon.stub().callsFake(async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('value from cache');
}, 200);
});
}),
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub,
getTimeoutMilliseconds: 100
});
const value = await cache.get('key');
assert.equal(value, null);
});
it('can update the cache in the case of a cache miss', async function () {
const KEY = 'update-cache-on-miss';
let cachedValue = null;
const redisCacheInstanceStub = {
get: function (key) {
assert(key === KEY);
return cachedValue;
},
set: function (key, value) {
assert(key === KEY);
cachedValue = value;
},
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub
});
const fetchData = sinon.stub().resolves('Da Value');
checkFirstRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 1);
assert.equal(value, 'Da Value');
break checkFirstRead;
}
checkSecondRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 1);
assert.equal(value, 'Da Value');
break checkSecondRead;
}
});
it('Can do a background update of the cache', async function () {
const KEY = 'update-cache-in-background';
let cachedValue = null;
let remainingTTL = 100;
const redisCacheInstanceStub = {
get: function (key) {
assert(key === KEY);
return cachedValue;
},
set: function (key, value) {
assert(key === KEY);
cachedValue = value;
},
ttl: function (key) {
assert(key === KEY);
return remainingTTL;
},
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub,
ttl: 100,
refreshAheadFactor: 0.2
});
const fetchData = sinon.stub();
fetchData.onFirstCall().resolves('First Value');
fetchData.onSecondCall().resolves('Second Value');
checkFirstRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 1);
assert.equal(value, 'First Value');
break checkFirstRead;
}
// We simulate having been in the cache for 15 seconds
remainingTTL = 85;
checkSecondRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 1);
assert.equal(value, 'First Value');
break checkSecondRead;
}
// We simulate having been in the cache for 30 seconds
remainingTTL = 70;
checkThirdRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 1);
assert.equal(value, 'First Value');
break checkThirdRead;
}
// We simulate having been in the cache for 85 seconds
// This should trigger a background refresh
remainingTTL = 15;
checkFourthRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 2);
assert.equal(value, 'First Value');
break checkFourthRead;
}
// We reset the TTL to 100 for the most recent write
remainingTTL = 100;
checkFifthRead: {
const value = await cache.get(KEY, fetchData);
assert.equal(fetchData.callCount, 2);
assert.equal(value, 'Second Value');
break checkFifthRead;
}
});
});
describe('set', function () {
it('can set a value in the cache', async function () {
const redisCacheInstanceStub = {
set: sinon.stub().resolvesArg(1),
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub
});
const value = await cache.set('key-here', 'new value');
assert.equal(value, 'new value');
assert.equal(redisCacheInstanceStub.set.args[0][0], 'key-here');
});
it('sets a key based on keyPrefix', async function () {
const redisCacheInstanceStub = {
set: sinon.stub().resolvesArg(1),
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub,
keyPrefix: 'testing-prefix:'
});
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');
});
});
describe('reset', function () {
it('catches an error when thrown during the reset', async function () {
const redisCacheInstanceStub = {
get: sinon.stub().resolves('value from cache'),
store: {
getClient: sinon.stub().returns({
on: sinon.stub()
})
}
};
const cache = new RedisCache({
cache: redisCacheInstanceStub
});
await cache.reset();
assert.ok(logging.error.calledOnce, 'error was logged');
});
});
});