Added optional in-memory TTL cache
refs https://github.com/TryGhost/Toolbox/issues/515 - We don't have a good way to test TTL caches without setting up Redis in the environment - Adding in-memory cache adapter with TTL allows to run tests on CI without having to install Redis - Also, TTL in memory cache can be a great substitution for Redis-based caches on instances that have a lot of spare RAM and don't need to use Redis necessarily - MemoryTTL cache accepts two parameters "TTL" and "max" - TTL - is time in milliseconds to hold the value for in cache - max - is the maximum amount of items to keep in the cache - To use MemoryTTL cache specify following config in the cache section: ``` "adapters": { "cache": { "imageSizes": { "adapter": "MemoryTTL", "ttl": 3600 } } } ``` - Above config would apply MemoryTTL cache to imageSizes feature with TTL fo 3600 ms
This commit is contained in:
parent
77a65fee61
commit
95530a6617
6
ghost/adapter-cache-memory-ttl/.eslintrc.js
Normal file
6
ghost/adapter-cache-memory-ttl/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/node'
|
||||
]
|
||||
};
|
23
ghost/adapter-cache-memory-ttl/README.md
Normal file
23
ghost/adapter-cache-memory-ttl/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Adapter Cache Memory Ttl
|
||||
|
||||
Cache adapter with in-memory storage with TTL functionality
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `yarn` to install top-level dependencies.
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
- `yarn lint` run just eslint
|
||||
- `yarn test` run lint and tests
|
||||
|
1
ghost/adapter-cache-memory-ttl/index.js
Normal file
1
ghost/adapter-cache-memory-ttl/index.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./lib/adapter-cache-memory-ttl');
|
@ -0,0 +1,54 @@
|
||||
const TTLCache = require('@isaacs/ttlcache');
|
||||
const Base = require('@tryghost/adapter-base-cache');
|
||||
|
||||
/**
|
||||
* Cache adapter compatible wrapper around TTLCache
|
||||
* Distinct features of this cache adapter:
|
||||
* - it is in-memory only
|
||||
* - it supports time-to-live (TTL)
|
||||
* - it supports a max number of items
|
||||
*/
|
||||
class MemoryTTL extends Base {
|
||||
#cache;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} [deps]
|
||||
* @param {Number} [deps.max] - The max number of items to keep in the cache.
|
||||
* @param {Number} [deps.ttl] - The max time in ms to store items
|
||||
*/
|
||||
constructor({max = Infinity, ttl = Infinity} = {}) {
|
||||
super();
|
||||
|
||||
this.#cache = new TTLCache({max, ttl});
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.#cache.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
* @param {Object} [options]
|
||||
* @param {Number} [options.ttl]
|
||||
*/
|
||||
set(key, value, {ttl} = {}) {
|
||||
this.#cache.set(key, value, {ttl});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assist "getAll" type of operations
|
||||
* @returns {Array<String>} all keys present in the cache
|
||||
*/
|
||||
keys() {
|
||||
return [...this.#cache.keys()];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MemoryTTL;
|
28
ghost/adapter-cache-memory-ttl/package.json
Normal file
28
ghost/adapter-cache-memory-ttl/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@tryghost/adapter-cache-memory-ttl",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/adapter-cache-memory-ttl",
|
||||
"author": "Ghost Foundation",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "echo \"Implement me!\"",
|
||||
"test:unit": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
||||
"test": "yarn test:unit",
|
||||
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
||||
"lint": "yarn lint:code && yarn lint:test",
|
||||
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"c8": "7.13.0",
|
||||
"mocha": "10.2.0",
|
||||
"sinon": "15.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "1.2.1"
|
||||
}
|
||||
}
|
6
ghost/adapter-cache-memory-ttl/test/.eslintrc.js
Normal file
6
ghost/adapter-cache-memory-ttl/test/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
]
|
||||
};
|
@ -0,0 +1,100 @@
|
||||
const assert = require('assert');
|
||||
const MemoryTTLCache = require('../index');
|
||||
|
||||
const sleep = ms => (
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
})
|
||||
);
|
||||
|
||||
describe('Cache Adapter In Memory with Time To Live', function () {
|
||||
it('Can initialize a cache instance', function () {
|
||||
const cache = new MemoryTTLCache();
|
||||
assert.ok(cache);
|
||||
});
|
||||
|
||||
describe('get', function () {
|
||||
it('Can get a value from the cache', async function () {
|
||||
const cache = new MemoryTTLCache({});
|
||||
cache.set('a', 'b');
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache after some time');
|
||||
});
|
||||
|
||||
it('Can get a value from the cache before TTL kicks in', async function () {
|
||||
const cache = new MemoryTTLCache({ttl: 150});
|
||||
cache.set('a', 'b');
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache before TTL time');
|
||||
|
||||
// NOTE: 100 + 100 = 200, which is more than 150 TTL
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', function () {
|
||||
it('Can set a value in the cache', async function () {
|
||||
const cache = new MemoryTTLCache({ttl: 150});
|
||||
|
||||
cache.set('a', 'b');
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache after time < TTL');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time');
|
||||
});
|
||||
|
||||
it('Can override TTL time', async function () {
|
||||
const cache = new MemoryTTLCache({ttl: 150});
|
||||
|
||||
cache.set('a', 'b', {ttl: 99});
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', function () {
|
||||
it('Can reset the cache', async function () {
|
||||
const cache = new MemoryTTLCache({ttl: 150});
|
||||
|
||||
cache.set('a', 'b');
|
||||
cache.set('c', 'd');
|
||||
|
||||
assert.equal(cache.get('a'), 'b', 'should get the value from the cache');
|
||||
assert.equal(cache.get('c'), 'd', 'should get the value from the cache');
|
||||
|
||||
cache.reset();
|
||||
|
||||
assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after reset');
|
||||
assert.equal(cache.get('c'), undefined, 'should NOT get the value from the cache after reset');
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', function () {
|
||||
it('Can get all keys from the cache', async function () {
|
||||
const cache = new MemoryTTLCache({ttl: 200});
|
||||
|
||||
cache.set('a', 'b');
|
||||
cache.set('c', 'd');
|
||||
|
||||
assert.deepEqual(cache.keys(), ['a', 'c'], 'should get all keys from the cache');
|
||||
});
|
||||
});
|
||||
});
|
3
ghost/core/core/server/adapters/cache/MemoryTTL.js
vendored
Normal file
3
ghost/core/core/server/adapters/cache/MemoryTTL.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
const TTLMemoryCache = require('@tryghost/adapter-cache-memory-ttl');
|
||||
|
||||
module.exports = TTLMemoryCache;
|
@ -2830,6 +2830,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@isaacs/ttlcache@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.2.1.tgz#07f54e31ee2dde9f0d2608fe3707f358596825e2"
|
||||
integrity sha512-6hUKl0TcmeenJXitePBS/vPn1l/C8+sO4vvSmRh/hW4CeBm+QselPM6AyiM7WON6jApouCJGUfHYbaNObcMFrQ==
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
|
Loading…
Reference in New Issue
Block a user