130 lines
4.9 KiB
JavaScript
130 lines
4.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { EventEmitter } from 'node:events';
|
|
import { createRedisCache, withCacheInvalidation } from '../src/cache/redis-cache.mjs';
|
|
|
|
class FakeRedisClient extends EventEmitter {
|
|
constructor({ connectError = null } = {}) {
|
|
super();
|
|
this.connectError = connectError;
|
|
this.isOpen = false;
|
|
this.isReady = false;
|
|
this.values = new Map();
|
|
}
|
|
|
|
async connect() {
|
|
if (this.connectError) throw this.connectError;
|
|
this.isOpen = true;
|
|
this.isReady = true;
|
|
this.emit('ready');
|
|
}
|
|
|
|
async get(key) {
|
|
return this.values.has(key) ? this.values.get(key) : null;
|
|
}
|
|
|
|
async set(key, value, options = {}) {
|
|
if (options.NX && this.values.has(key)) return null;
|
|
this.values.set(key, value);
|
|
return 'OK';
|
|
}
|
|
|
|
async incr(key) {
|
|
const next = Number(this.values.get(key) || 0) + 1;
|
|
this.values.set(key, String(next));
|
|
return next;
|
|
}
|
|
|
|
async quit() {
|
|
this.isReady = false;
|
|
this.isOpen = false;
|
|
}
|
|
|
|
destroy() {
|
|
this.isReady = false;
|
|
this.isOpen = false;
|
|
}
|
|
}
|
|
|
|
const silentLogger = { warn() {} };
|
|
|
|
{
|
|
const client = new FakeRedisClient();
|
|
const cache = await createRedisCache({
|
|
env: { REDIS_URL: 'redis://test', REDIS_CACHE_PREFIX: 'test', REDIS_CACHE_TTL_SECONDS: '30' },
|
|
logger: silentLogger,
|
|
clientFactory: () => client
|
|
});
|
|
let loads = 0;
|
|
const load = async () => ({ version: ++loads });
|
|
|
|
assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 });
|
|
assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 });
|
|
assert.equal(loads, 1, '相同缓存键应只读取一次数据源');
|
|
|
|
await cache.invalidate('public');
|
|
assert.deepEqual(await cache.remember('public', 'home', load), { version: 2 });
|
|
assert.equal(loads, 2, '命名空间失效后应重新读取数据源');
|
|
|
|
let resultLoads = 0;
|
|
const loadResults = async () => ({ version: ++resultLoads });
|
|
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 1 });
|
|
|
|
const database = withCacheInvalidation({
|
|
client: 'test',
|
|
async read() { return {}; },
|
|
async save() { return 'saved'; },
|
|
async close() {}
|
|
}, cache, method => method === 'save' ? ['public', 'results'] : ['public']);
|
|
assert.equal(await database.save(), 'saved');
|
|
assert.deepEqual(await cache.remember('public', 'home', load), { version: 3 });
|
|
assert.equal(loads, 3, '数据库写入后应让公开缓存失效');
|
|
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 2 });
|
|
assert.equal(resultLoads, 2, '成绩相关写入后应让成绩缓存失效');
|
|
await cache.invalidate('results');
|
|
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 3 });
|
|
assert.equal(resultLoads, 3, '后台手动刷新后应重新生成成绩缓存');
|
|
|
|
client.isReady = false;
|
|
let fallbackLoads = 0;
|
|
assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1);
|
|
assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1);
|
|
assert.equal(fallbackLoads, 1, 'Redis 运行中断开后,相同热点读取应由本机缓存合并');
|
|
client.isReady = true;
|
|
await cache.close();
|
|
}
|
|
|
|
{
|
|
const cache = await createRedisCache({ env: {}, logger: silentLogger });
|
|
let loads = 0;
|
|
assert.equal(await cache.remember('public', 'home', async () => ++loads), 1);
|
|
assert.equal(await cache.remember('public', 'home', async () => ++loads), 1);
|
|
assert.equal(loads, 1, '未配置 Redis 时应使用有界本机缓存,避免重复回源');
|
|
await cache.invalidate('public');
|
|
assert.equal(await cache.remember('public', 'home', async () => ++loads), 2);
|
|
assert.equal(loads, 2, '本机缓存应在数据库写入后立即失效');
|
|
assert.equal(cache.status, 'disabled');
|
|
|
|
let releaseOld;
|
|
const oldLoad = cache.remember('public', 'race', () => new Promise(resolve => { releaseOld = () => resolve('old'); }));
|
|
await cache.invalidate('public');
|
|
const newLoad = cache.remember('public', 'race', async () => 'new');
|
|
releaseOld();
|
|
assert.equal(await oldLoad, 'old');
|
|
assert.equal(await newLoad, 'new', '失效后不得等待失效前仍在运行的加载');
|
|
assert.equal(await cache.remember('public', 'race', async () => 'unexpected'), 'new', '旧加载完成后不得覆盖新缓存');
|
|
}
|
|
|
|
{
|
|
const client = new FakeRedisClient({ connectError: new Error('connection refused') });
|
|
const cache = await createRedisCache({
|
|
env: { REDIS_URL: 'redis://unavailable' },
|
|
logger: silentLogger,
|
|
clientFactory: () => client
|
|
});
|
|
assert.equal(cache.status, 'unavailable');
|
|
assert.equal(await cache.remember('public', 'home', async () => 'database'), 'database');
|
|
assert.equal(await cache.remember('public', 'home', async () => 'unexpected'), 'database', 'Redis 故障时本机缓存应继续承接重复读取');
|
|
}
|
|
|
|
console.log('Redis 缓存测试通过');
|