From 3f1f8bb689d1cd6c6d05ff8c2faaa5f811038c64 Mon Sep 17 00:00:00 2001 From: Stephane Hervochon Date: Thu, 19 Aug 2021 14:58:55 +0200 Subject: [PATCH] Cache improvement (#7) * Adjust cache to allow filtering of parameters for cache key calculation * Fix dependency issues --- docs/content/api/module/cache.md | 1 + package-lock.json | 6 ++-- src/index.ts | 3 +- src/module/cache.ts | 25 +++++++++++++++-- test/unit/module/cache.spec.ts | 48 ++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/docs/content/api/module/cache.md b/docs/content/api/module/cache.md index bd416a9..ffdc8c3 100644 --- a/docs/content/api/module/cache.md +++ b/docs/content/api/module/cache.md @@ -64,6 +64,7 @@ circuit.fn(myFirstFunction).execute(myObject) | `ttl` | The amount of time during which a cached result is considered valid. | `6000` | | `cacheClearInterval` | The amount of time before the cache cleans itself up. | `900000` | | `getInformationFromCache` | Specifies if the async response contains information if the data is retrieved from Cache (in this case, the information is available in _mollitiaIsFromCache property) | false | +| `adjustCacheParams` | A filtering callback, to modify the parameters used for Cache Key. | `none` | ## Events diff --git a/package-lock.json b/package-lock.json index 72a850c..3fb6c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5716,9 +5716,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-type": { diff --git a/src/index.ts b/src/index.ts index 017b109..cf9379a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { circuits, Circuit, CircuitFactory, CircuitOptions, NoFuncError } from './circuit'; +import { circuits, Circuit, CircuitFactory, CircuitFunction, CircuitOptions, NoFuncError } from './circuit'; import { use, Addon } from './addon'; import { modules, Module, ModuleOptions } from './module/index'; import { Timeout, TimeoutError, TimeoutOptions } from './module/timeout'; @@ -17,6 +17,7 @@ export { circuits, Circuit, CircuitFactory, + CircuitFunction, CircuitOptions, NoFuncError, // Module diff --git a/src/module/cache.ts b/src/module/cache.ts index ba87ca3..c02ddd6 100644 --- a/src/module/cache.ts +++ b/src/module/cache.ts @@ -2,6 +2,9 @@ import { Module, ModuleOptions } from '.'; import { Circuit, CircuitFunction } from '../circuit'; import { MapCache } from '../helpers/map-cache'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AdjustCacheParamsCallback = (func: CircuitFunction, ...params: any[]) => any; + /** * Properties that customizes the cache behavior. */ @@ -18,6 +21,12 @@ export abstract class CacheOptions extends ModuleOptions { * The attribute name indicating if data is retrieved from cache or not */ getInformationFromCache? : boolean; + + /** + * A filtering callback, to modify the parameters used for Cache Key. + * @returns The modified parameters + */ + adjustCacheParams?: AdjustCacheParamsCallback; } type CacheT = T & { @@ -37,6 +46,11 @@ export class Cache extends Module { * The attribute name indicating if data is retrieved from cache or not */ public getInformationFromCache: boolean; + + /** + * A filtering callback, to modify the parameters used for Cache Key. + */ + public adjustCacheParams: AdjustCacheParamsCallback|null; // Private Attributes private cache: MapCache; private _cacheClearInterval: number; @@ -60,6 +74,7 @@ export class Cache extends Module { super(options); this.ttl = (options?.ttl !== undefined) ? options.ttl : 6000; // 1 minute this.getInformationFromCache = (options?.getInformationFromCache !== undefined) ? options.getInformationFromCache : false; + this.adjustCacheParams = options?.adjustCacheParams || null; this._cacheInterval = null; this._cacheClearInterval = 0; this.cacheClearInterval = (options?.cacheClearInterval !== undefined) ? options.cacheClearInterval : 900000; // 15 minutes @@ -84,7 +99,11 @@ export class Cache extends Module { private async _promiseCache (circuit: Circuit, promise: CircuitFunction, ...params: any[]): Promise> { return new Promise((resolve, reject) => { const cacheParams = this.getExecParams(circuit, params); - const cacheRes = this.cache.get>(circuit.func, ...cacheParams); + let cacheKey = cacheParams; + if (this.adjustCacheParams) { + cacheKey = this.adjustCacheParams(circuit.func, ...cacheParams); + } + const cacheRes = this.cache.get>(circuit.func, ...cacheKey); if (cacheRes) { if (typeof cacheRes.res === 'object' && this.getInformationFromCache) { cacheRes.res._mollitiaIsFromCache = true; @@ -94,7 +113,7 @@ export class Cache extends Module { promise(...params) .then((res: CacheT) => { if (this.ttl > 0) { - this.cache.set(this.ttl, circuit.func, ...cacheParams, res); + this.cache.set(this.ttl, circuit.func, ...cacheKey, res); } if (typeof res === 'object' && this.getInformationFromCache) { res._mollitiaIsFromCache = false; @@ -113,7 +132,7 @@ export class Cache extends Module { promise(...params) .then((res: CacheT) => { if (this.ttl > 0) { - this.cache.set(this.ttl, circuit.func, ...cacheParams, res); + this.cache.set(this.ttl, circuit.func, ...cacheKey, res); } if (typeof res === 'object' && this.getInformationFromCache) { res._mollitiaIsFromCache = false; diff --git a/test/unit/module/cache.spec.ts b/test/unit/module/cache.spec.ts index 18bc453..ce023a7 100644 --- a/test/unit/module/cache.spec.ts +++ b/test/unit/module/cache.spec.ts @@ -23,6 +23,14 @@ const failureAsync = jest.fn().mockImplementation((res: unknown = 'default', del }); }); +const successAsync2 = jest.fn().mockImplementation((res: unknown = 'default', res2: unknown = 'default', delay = 1) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({res, res2}); + }, delay); + }); +}); + describe('Cache', () => { afterEach(() => { logger.debug.mockClear(); @@ -67,6 +75,46 @@ describe('Cache', () => { circuit.dispose(); }); + it('should cache the previous response - using onCache parameter', async () => { + const cacheModule = new Mollitia.Cache({ + name: 'module-cache', + logger, + ttl: 100 + }); + const circuit = new Mollitia.Circuit({ + name: 'circuit-cache', + options: { + modules: [ cacheModule ] + } + }); + const objRef = { dummy: 'value1' }; + const objRef2 = { dummy2: 'value2' }; + const objRef3 = { dummy3: 'value3' }; + await expect(circuit.fn(successAsync2).execute(objRef, objRef2)).resolves.toEqual({res: objRef, res2: objRef2}); + await expect(circuit.fn(successAsync2).execute(objRef, objRef3)).resolves.toEqual({res: objRef, res2: objRef3}); + expect(logger.debug).not.toHaveBeenNthCalledWith(1, 'circuit-cache/module-cache - Cache: Hit'); + + await delay(150); + cacheModule.adjustCacheParams = (circuitFunction: Mollitia.CircuitFunction, ...params) => { + return params.slice(1); + }; + await expect(circuit.fn(successAsync2).execute(objRef, objRef2)).resolves.toEqual({res: objRef, res2: objRef2}); + await expect(circuit.fn(successAsync2).execute(objRef, objRef3)).resolves.toEqual({res: objRef, res2: objRef3}); + expect(logger.debug).not.toHaveBeenNthCalledWith(1, 'circuit-cache/module-cache - Cache: Hit'); + await expect(circuit.fn(successAsync2).execute(objRef2, objRef)).resolves.toEqual({res: objRef2, res2: objRef}); + await expect(circuit.fn(successAsync2).execute(objRef3, objRef)).resolves.toEqual({res: objRef2, res2: objRef}); + expect(logger.debug).toHaveBeenNthCalledWith(1, 'circuit-cache/module-cache - Cache: Hit'); + + await delay(150); + cacheModule.adjustCacheParams = (circuitFunction: Mollitia.CircuitFunction, ...params) => { + return params.slice(0,1); + }; + await expect(circuit.fn(successAsync2).execute(objRef, objRef2)).resolves.toEqual({res: objRef, res2: objRef2}); + await expect(circuit.fn(successAsync2).execute(objRef, objRef3)).resolves.toEqual({res: objRef, res2: objRef2}); + expect(logger.debug).toHaveBeenNthCalledWith(2, 'circuit-cache/module-cache - Cache: Hit'); + circuit.dispose(); + }); + it('With multiple modules - Cache module in the middle of the modules - Cache the previous response by reference', async () => { const moduleRetry = new Mollitia.Retry({ attempts: 2