test: utils cache

This commit is contained in:
DIYgod
2024-02-26 17:05:33 +08:00
parent 616f26a49d
commit 5ea8712345
4 changed files with 134 additions and 26 deletions
+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it, jest, afterEach, afterAll, beforeAll } from '@jest/globals';
import wait from '@/utils/wait';
beforeAll(() => {
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '3';
});
afterEach(() => {
delete process.env.CACHE_TYPE;
jest.resetModules();
});
afterAll(() => {
delete process.env.CACHE_EXPIRE;
});
describe('cache', () => {
it('memory', async () => {
process.env.CACHE_TYPE = 'memory';
const cache = (await import('@/utils/cache')).default;
if (!cache.clients.memoryCache || !cache.status.available) {
throw new Error('Memory cache client error');
}
await cache.set('mock', undefined);
expect(await cache.get('mock')).toBe('');
await cache.globalCache.set('mock', undefined);
expect(await cache.globalCache.get('mock')).toBe('');
await cache.globalCache.set('mock', {
mock: 1,
});
expect(await cache.globalCache.get('mock')).toBe('{"mock":1}');
}, 10000);
it('redis', async () => {
process.env.CACHE_TYPE = 'redis';
process.env.REDIS_URL = 'redis://localhost:6380';
const cache = (await import('@/utils/cache')).default;
await wait(500);
if (!cache.clients.redisClient || !cache.status.available) {
throw new Error('Redis client error');
}
await cache.set('mock1', undefined);
expect(await cache.get('mock1')).toBe('');
await cache.set('mock2', '2');
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe('2');
await cache.clients.redisClient?.quit();
}, 10000);
it('redis with quit', async () => {
process.env.CACHE_TYPE = 'redis';
const cache = (await import('@/utils/cache')).default;
if (cache.clients.redisClient) {
await cache.clients.redisClient.quit();
} else {
throw new Error('No redis client');
}
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
});
it('redis with error', async () => {
process.env.CACHE_TYPE = 'redis';
process.env.REDIS_URL = 'redis://wrongpath:6379';
const cache = (await import('@/utils/cache')).default;
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
await cache.clients.redisClient?.quit();
});
it('no cache', async () => {
process.env.CACHE_TYPE = 'NO';
const cache = (await import('@/utils/cache')).default;
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
});
it('throws TTL key', async () => {
process.env.CACHE_TYPE = 'redis';
const cache = (await import('@/utils/cache')).default;
try {
await cache.get('rsshub:cacheTtl:mock');
} catch (error: any) {
expect(error.message).toContain('reserved for the internal usage');
} finally {
await cache.clients.redisClient?.quit();
}
});
});
+13 -1
View File
@@ -2,6 +2,7 @@ import { config } from '@/config';
import redis from './redis';
import memory from './memory';
import type CacheModule from './base';
import logger from '@/utils/logger';
const globalCache: {
get: (key: string) => Promise<string | null | undefined> | string | null | undefined;
@@ -24,7 +25,7 @@ if (config.cache.type === 'redis') {
}
};
globalCache.set = cacheModule.set;
} else {
} else if (config.cache.type === 'memory') {
cacheModule = memory;
cacheModule.init();
const { memoryCache } = cacheModule.clients;
@@ -44,6 +45,17 @@ if (config.cache.type === 'redis') {
return memoryCache.set(key, value, { ttl: maxAge * 1000 });
}
};
} else {
cacheModule = {
init: () => null,
get: () => null,
set: () => null,
status: {
available: false,
},
clients: {},
};
logger.error('Cache not available, concurrent requests are not limited. This could lead to bad behavior.');
}
// only give cache string, as the `!` condition tricky
+11 -8
View File
@@ -3,24 +3,27 @@ import { config } from '@/config';
import type CacheModule from './base';
const status = { available: false };
let memoryCache: LRUCache<any, any> | undefined;
const clients: {
memoryCache?: LRUCache<any, any>;
} = {};
export default {
init: () => {
memoryCache = new LRUCache({
clients.memoryCache = new LRUCache({
ttl: config.cache.routeExpire * 1000,
max: config.memory.max,
});
status.available = true;
},
get: (key: string, refresh = true) => {
if (key && status.available && memoryCache) {
let value = memoryCache.get(key, { updateAgeOnGet: refresh }) as string | undefined;
if (key && status.available && clients.memoryCache) {
let value = clients.memoryCache.get(key, { updateAgeOnGet: refresh }) as string | undefined;
if (value) {
value = value + '';
}
return value;
} else {
return null;
}
},
set: (key, value, maxAge = config.cache.contentExpire) => {
@@ -30,10 +33,10 @@ export default {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
if (key && status.available && memoryCache) {
return memoryCache.set(key, value, { ttl: maxAge * 1000 });
if (key && status.available && clients.memoryCache) {
return clients.memoryCache.set(key, value, { ttl: maxAge * 1000 });
}
},
clients: { memoryCache },
clients,
status,
} as CacheModule;
+18 -17
View File
@@ -3,9 +3,10 @@ import Redis from 'ioredis';
import logger from '@/utils/logger';
import type CacheModule from './base';
let redisClient: Redis | undefined;
const status = { available: false };
const clients: {
redisClient?: Redis;
} = {};
const getCacheTtlKey = (key: string) => {
if (key.startsWith('rsshub:cacheTtl:')) {
@@ -16,43 +17,43 @@ const getCacheTtlKey = (key: string) => {
export default {
init: () => {
redisClient = new Redis(config.redis.url);
clients.redisClient = new Redis(config.redis.url);
const status = { available: false };
redisClient.on('error', (error) => {
clients.redisClient.on('error', (error) => {
status.available = false;
logger.error('Redis error: ', error);
});
redisClient.on('end', () => {
clients.redisClient.on('end', () => {
status.available = false;
});
redisClient.on('connect', () => {
clients.redisClient.on('connect', () => {
status.available = true;
logger.info('Redis connected.');
});
},
get: async (key: string, refresh = true) => {
if (key && status.available && redisClient) {
if (key && status.available && clients.redisClient) {
const cacheTtlKey = getCacheTtlKey(key);
let [value, cacheTtl] = await redisClient.mget(key, cacheTtlKey);
let [value, cacheTtl] = await clients.redisClient.mget(key, cacheTtlKey);
if (value && refresh) {
if (cacheTtl) {
redisClient.expire(cacheTtlKey, cacheTtl);
clients.redisClient.expire(cacheTtlKey, cacheTtl);
} else {
// if cacheTtl is not set, that means the cache expire time is contentExpire
cacheTtl = config.cache.contentExpire + '';
// dont save cacheTtl to Redis, as it is the default value
// redisClient.set(cacheTtlKey, cacheTtl, 'EX', cacheTtl);
}
redisClient.expire(key, cacheTtl);
clients.redisClient.expire(key, cacheTtl);
value = value + '';
}
return value;
return value || '';
} else {
return null;
}
},
set: (key: string, value?: string | Record<string, any>, maxAge = config.cache.contentExpire) => {
if (!status.available || !redisClient) {
if (!status.available || !clients.redisClient) {
return;
}
if (!value || value === 'undefined') {
@@ -64,11 +65,11 @@ export default {
if (key) {
if (maxAge !== config.cache.contentExpire) {
// Only set cacheTtlKey if maxAge !== contentExpire
redisClient.set(getCacheTtlKey(key), maxAge, 'EX', maxAge);
clients.redisClient.set(getCacheTtlKey(key), maxAge, 'EX', maxAge);
}
return redisClient.set(key, value, 'EX', maxAge); // setMode: https://redis.io/commands/set
return clients.redisClient.set(key, value, 'EX', maxAge); // setMode: https://redis.io/commands/set
}
},
clients: { redisClient },
clients,
status,
} as CacheModule;