Skip to content

Commit

Permalink
refactor(cache): improve cache (nocobase#3004)
Browse files Browse the repository at this point in the history
* feat: improve cache

* fix: bug

* fix: test

* fix: test

* fix: test

* chore: add cache test

* feat: add wrapWithCondition

* fix: test

* refactor: improve api

* fix: test

* fix: test

* fix: test

* fix: improve code

* fix: test

* feat: register redis store

* fix: tst

* fix: test

* fix: bug

* chore: update

* fix: ttl unit

* chore: cachemanager constructor

* chore: remove code

* feat: support close connection

* chore: add close options for redis store
  • Loading branch information
2013xile authored Nov 20, 2023
1 parent 379248e commit daac2ae
Show file tree
Hide file tree
Showing 21 changed files with 540 additions and 247 deletions.
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ DB_TABLE_PREFIX=
# DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED=true

################# CACHE #################
# default is memory cache, when develop mode,code's change will be clear memory cache, so can use 'cache-manager-fs-hash'
# CACHE_CONFIG={"storePackage":"cache-manager-fs-hash","ttl":86400,"max":1000}
CACHE_DEFAULT_STORE=memory
# max number of items in memory cache
CACHE_MEMORY_MAX=2000
# CACHE_REDIS_URL=

################# STORAGE (Initialization only) #################

Expand Down
21 changes: 17 additions & 4 deletions packages/core/app/src/config/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { createDefaultCacheConfig } from '@nocobase/cache';
import { CacheManagerOptions } from '@nocobase/cache';

const cacheConfig = process.env.CACHE_CONFIG ? JSON.parse(process.env.CACHE_CONFIG) : createDefaultCacheConfig();

export default cacheConfig;
export const cacheManager = {
defaultStore: process.env.CACHE_DEFAULT_STORE || 'memory',
stores: {
memory: {
store: 'memory',
max: parseInt(process.env.CACHE_MEMORY_MAX) || 2000,
},
...(process.env.CACHE_REDIS_URL
? {
redis: {
url: process.env.CACHE_REDIS_URL,
},
}
: {}),
},
} as CacheManagerOptions;
4 changes: 2 additions & 2 deletions packages/core/app/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cache from './cache';
import { cacheManager } from './cache';
import { parseDatabaseOptions } from './database';
import logger from './logger';
import plugins from './plugins';
Expand All @@ -9,7 +9,7 @@ export async function getConfig() {
database: await parseDatabaseOptions(),
resourcer,
plugins,
cache,
cacheManager,
logger,
};
}
6 changes: 3 additions & 3 deletions packages/core/cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"cache-manager": "^4.1.0"
"cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2"
},
"devDependencies": {
"@types/cache-manager": "^4.0.2",
"cache-manager-fs-hash": "^1.0.0"
"redis": "^4.6.10"
},
"repository": {
"type": "git",
Expand Down
42 changes: 42 additions & 0 deletions packages/core/cache/src/__tests__/cache-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Cache } from '../cache';
import { CacheManager } from '../cache-manager';

describe('cache-manager', () => {
let cacheManager: CacheManager;

beforeEach(() => {
cacheManager = new CacheManager();
});

afterEach(() => {
cacheManager = null;
});

it('create with default config', async () => {
cacheManager.registerStore({ name: 'memory', store: 'memory' });
const cache = await cacheManager.createCache({ name: 'test', store: 'memory' });
expect(cache).toBeDefined();
expect(cache.name).toBe('test');
expect(cacheManager.caches.has('test')).toBeTruthy();
});

it('create with custom config', async () => {
cacheManager.registerStore({ name: 'memory', store: 'memory' });
const cache = (await cacheManager.createCache({ name: 'test', store: 'memory', ttl: 100 })) as Cache;
expect(cache).toBeDefined();
expect(cache.name).toBe('test');
expect(cacheManager.caches.has('test')).toBeTruthy();
});

it('should close store', async () => {
const close = jest.fn();
cacheManager.registerStore({
name: 'memory',
store: 'memory',
close,
});
await cacheManager.createCache({ name: 'test', store: 'memory' });
await cacheManager.close();
expect(close).toBeCalled();
});
});
64 changes: 64 additions & 0 deletions packages/core/cache/src/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Cache } from '../cache';
import { CacheManager } from '../cache-manager';
import lodash from 'lodash';

describe('cache', () => {
let cache: Cache;

beforeEach(async () => {
const cacheManager = new CacheManager();
cacheManager.registerStore({ name: 'memory', store: 'memory' });
cache = await cacheManager.createCache({ name: 'test', store: 'memory' });
});

afterEach(async () => {
await cache.reset();
});

it('should set and get value', async () => {
await cache.set('key', 'value');
const value = await cache.get('key');
expect(value).toBe('value');
});

it('set and get value in object', async () => {
const value = { a: 1 };
await cache.set('key', value);
const cacheA = await cache.getValueInObject('key', 'a');
expect(cacheA).toEqual(1);

await cache.setValueInObject('key', 'a', 2);
const cacheVal2 = await cache.getValueInObject('key', 'a');
expect(cacheVal2).toEqual(2);
});

it('wrap with condition, useCache', async () => {
const obj = {};
const get = () => obj;
const val = await cache.wrapWithCondition('key', get, {
useCache: false,
});
expect(val).toBe(obj);
expect(await cache.get('key')).toBeUndefined();
const val2 = await cache.wrapWithCondition('key', get);
expect(val2).toBe(obj);
expect(await cache.get('key')).toMatchObject(obj);
});

it('wrap with condition, isCacheable', async () => {
let obj = {};
const get = () => obj;
const isCacheable = (val: any) => !lodash.isEmpty(val);
const val = await cache.wrapWithCondition('key', get, {
isCacheable,
});
expect(val).toBe(obj);
expect(await cache.get('key')).toBeUndefined();
obj = { a: 1 };
const val2 = await cache.wrapWithCondition('key', get, {
isCacheable,
});
expect(val2).toBe(obj);
expect(await cache.get('key')).toMatchObject(obj);
});
});
72 changes: 0 additions & 72 deletions packages/core/cache/src/__tests__/index.test.ts

This file was deleted.

121 changes: 121 additions & 0 deletions packages/core/cache/src/cache-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { FactoryStore, Store, caching, Cache as BasicCache } from 'cache-manager';
import { Cache } from './cache';
import lodash from 'lodash';
import { RedisStore, redisStore } from 'cache-manager-redis-yet';
import deepmerge from 'deepmerge';

type StoreOptions = {
store?: 'memory' | FactoryStore<Store, any>;
close?: (store: Store) => Promise<void>;
// global config
[key: string]: any;
};

export type CacheManagerOptions = Partial<{
defaultStore: string;
stores: {
[storeType: string]: StoreOptions;
};
}>;

export class CacheManager {
defaultStore: string;
private stores = new Map<
string,
{
store: BasicCache;
close?: (store: Store) => Promise<void>;
}
>();
storeTypes = new Map<string, StoreOptions>();
caches = new Map<string, Cache>();

constructor(options?: CacheManagerOptions) {
const defaultOptions: CacheManagerOptions = {
defaultStore: 'memory',
stores: {
memory: {
store: 'memory',
// global config
max: 2000,
},
redis: {
store: redisStore,
close: async (redis: RedisStore) => {
await redis.client.quit();
},
},
},
};
const cacheOptions = deepmerge(defaultOptions, options || {});
const { defaultStore = 'memory', stores } = cacheOptions;
this.defaultStore = defaultStore;
for (const [name, store] of Object.entries(stores)) {
const { store: s, ...globalConfig } = store;
this.registerStore({ name, store: s, ...globalConfig });
}
}

private async createStore(options: { name: string; storeType: string; [key: string]: any }) {
const { name, storeType: type, ...config } = options;
const storeType = this.storeTypes.get(type) as any;
if (!storeType) {
throw new Error(`Create cache failed, store type [${type}] is unavailable or not registered`);
}
const { store: s, close, ...globalConfig } = storeType;
const store = await caching(s, { ...globalConfig, ...config });
this.stores.set(name, { close, store });
return store;
}

registerStore(options: { name: string } & StoreOptions) {
const { name, ...rest } = options;
this.storeTypes.set(name, rest);
}

private newCache(options: { name: string; prefix?: string; store: BasicCache }) {
const { name, prefix, store } = options;
const cache = new Cache({ name, prefix, store });
this.caches.set(name, cache);
return cache;
}

async createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }) {
const { name, prefix, store = this.defaultStore, ...config } = options;
if (!lodash.isEmpty(config)) {
const newStore = await this.createStore({ name, storeType: store, ...config });
return this.newCache({ name, prefix, store: newStore });
}
const s = this.stores.get(store);
if (!s) {
const defaultStore = await this.createStore({ name: store, storeType: store });
return this.newCache({ name, prefix, store: defaultStore });
}
return this.newCache({ name, prefix, store: s.store });
}

getCache(name: string): Cache {
const cache = this.caches.get(name);
if (!cache) {
throw new Error(`Get cache failed, ${name} is not found`);
}
return cache;
}

async flushAll() {
const promises = [];
for (const cache of this.caches.values()) {
promises.push(cache.reset());
}
await Promise.all(promises);
}

async close() {
const promises = [];
for (const s of this.stores.values()) {
const { close, store } = s;
close && promises.push(close(store.store));
}
await Promise.all(promises);
}
}
Loading

0 comments on commit daac2ae

Please sign in to comment.