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:
Naz 2023-02-09 21:21:00 +08:00
parent 77a65fee61
commit 95530a6617
No known key found for this signature in database
9 changed files with 226 additions and 0 deletions

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

View 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

View File

@ -0,0 +1 @@
module.exports = require('./lib/adapter-cache-memory-ttl');

View File

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

View 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"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View File

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

View File

@ -0,0 +1,3 @@
const TTLMemoryCache = require('@tryghost/adapter-cache-memory-ttl');
module.exports = TTLMemoryCache;

View File

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