mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-01 14:57:14 +08:00
test: utils cache
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Vendored
+13
-1
@@ -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
|
||||
|
||||
Vendored
+11
-8
@@ -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;
|
||||
|
||||
Vendored
+18
-17
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user