Skip to content

Commit

Permalink
feat: Tags can be stored separately in case the main redis instance u…
Browse files Browse the repository at this point in the history
…ses eviction policy.
  • Loading branch information
alexkvak committed Jan 29, 2020
1 parent 32e4da9 commit c25ae76
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 2 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface CacheWithCustomStorageOptions {
}
export interface CacheWithBaseStorageOptions {
adapter: StorageAdapter;
tagsAdapter?: StorageAdapter;
}
export interface ManagerConstructor<T extends BaseManager = any> {
new(options: ManagerOptions): T;
Expand Down Expand Up @@ -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
});
Expand Down
23 changes: 23 additions & 0 deletions src/storages/base.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
12 changes: 10 additions & 2 deletions src/storages/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -94,7 +102,7 @@ export class BaseStorage implements Storage {
*/
public async setTagVersions(tags: StorageRecordTag[]): Promise<any> {
const values = new Map(tags.map(tag => [this.createTagKey(tag.name), `${tag.version}`]));
return this.adapter.mset(values);
return this.tagsAdapter.mset(values);
}

/**
Expand Down Expand Up @@ -137,7 +145,7 @@ export class BaseStorage implements Storage {
* version.
*/
public async getTags(tagNames: string[]): Promise<StorageRecordTag[]> {
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,
Expand Down

0 comments on commit c25ae76

Please sign in to comment.