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"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
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":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
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