From c25ae76fad1ed24d27438933ff84f2516df50780 Mon Sep 17 00:00:00 2001 From: Aleksey Kvak Date: Wed, 29 Jan 2020 15:08:09 +0300 Subject: [PATCH] feat: Tags can be stored separately in case the main redis instance uses eviction policy. --- README.md | 19 +++++++++++++++++++ src/cache.ts | 2 ++ src/storages/base.spec.ts | 23 +++++++++++++++++++++++ src/storages/base.ts | 12 ++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 791bae7..eab7259 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,25 @@ export const cache = new Cache({ logger, }); ``` +If your Redis instance uses eviction policy you need to use separate Redis instance for tags. **Tags should not be evicted!** + +```typescript +// cache-with-tags.ts + +import Redis from 'ioredis'; +import Cache, { RedisStorageAdapter } from 'cachalot'; +import logger from './logger'; + +const redis = new Redis(); // with eviction policy enabled +const redisForTags = new Redis(6380); + +export const cache = new Cache({ + adapter: new RedisStorageAdapter(redis), + tagsAdapter: new RedisStorageAdapter(redisForTags), + logger, +}); +``` + There are three main methods of working with Cache; their behavior depends on the chosen caching strategy: `get` gets cache data diff --git a/src/cache.ts b/src/cache.ts index 82e76a8..299dc53 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -15,6 +15,7 @@ export interface CacheWithCustomStorageOptions { } export interface CacheWithBaseStorageOptions { adapter: StorageAdapter; + tagsAdapter?: StorageAdapter; } export interface ManagerConstructor { new(options: ManagerOptions): T; @@ -60,6 +61,7 @@ class Cache { if (isBaseStorageOptions(options)) { this.storage = new BaseStorage({ adapter: options.adapter, + tagsAdapter: options.tagsAdapter, prefix: options.prefix, hashKeys: options.hashKeys }); diff --git a/src/storages/base.spec.ts b/src/storages/base.spec.ts index 986e278..f3b3edd 100644 --- a/src/storages/base.spec.ts +++ b/src/storages/base.spec.ts @@ -318,4 +318,27 @@ describe('BaseStorage', () => { expect(value.tags).toMatchObject([{ name: 'tag' }]); expect(value.expiresIn).toEqual(expect.any(Number)); }); + + it('uses separate adapter for tags', async() => { + const tag1 = { name: 'tag1', version: 1 }; + const tagsTestInterface = { + internalStorage: {} + }; + const tagsTestAdapter = new TestStorageAdapter(tagsTestInterface, true); + tagsTestInterface.internalStorage[`cache-${TAGS_VERSIONS_ALIAS}:tag1`] = tag1.version; + storage = new BaseStorage({ + adapter: testAdapter, + tagsAdapter: tagsTestAdapter, + prefix: 'cache', + hashKeys: false, + expiresIn: 10000 + }); + + const tags = await storage.getTags([tag1.name]); + expect(tags).toEqual([tag1]); + + const tagV2 = { ...tag1, version: 2 }; + await storage.setTagVersions([tagV2]); + await expect(storage.getTags([tag1.name])).resolves.toEqual([tagV2]); + }); }); diff --git a/src/storages/base.ts b/src/storages/base.ts index a8f73a6..bdfb3e6 100644 --- a/src/storages/base.ts +++ b/src/storages/base.ts @@ -13,6 +13,7 @@ export const TAGS_VERSIONS_ALIAS = 'cache-tags-versions'; export type BaseStorageOptions = { adapter: StorageAdapter; + tagsAdapter?: StorageAdapter; prefix?: string; hashKeys?: boolean; expiresIn?: number; @@ -43,6 +44,7 @@ export type CommandFn = (...args: any[]) => any; export class BaseStorage implements Storage { constructor(options: BaseStorageOptions) { this.adapter = options.adapter; + this.tagsAdapter = options.tagsAdapter ?? options.adapter; this.prefix = options.prefix || ''; this.hashKeys = options.hashKeys || false; @@ -75,6 +77,12 @@ export class BaseStorage implements Storage { */ private adapter: StorageAdapter; + /** + * Adapter for tags should be provided if your primary adapter uses eviction policy. + * This adapter should not use any eviction policy. Records should be deleted only by demand or expiration. + */ + private readonly tagsAdapter: StorageAdapter; + /** * Gets a record using an adapter. It is expected that the adapter returns or null (value not found) * or serialized StorageRecord. @@ -94,7 +102,7 @@ export class BaseStorage implements Storage { */ public async setTagVersions(tags: StorageRecordTag[]): Promise { const values = new Map(tags.map(tag => [this.createTagKey(tag.name), `${tag.version}`])); - return this.adapter.mset(values); + return this.tagsAdapter.mset(values); } /** @@ -137,7 +145,7 @@ export class BaseStorage implements Storage { * version. */ public async getTags(tagNames: string[]): Promise { - const existingTags = await this.adapter.mget(tagNames.map(tagName => this.createTagKey(tagName))); + const existingTags = await this.tagsAdapter.mget(tagNames.map(tagName => this.createTagKey(tagName))); return tagNames.map((tagName, index) => ({ name: tagName,