From 1bd8e3b42532d4e95fc79c5a861cafedfc3ba29e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:59:05 +0000 Subject: [PATCH 1/3] Initial plan From a7261debc2c98b50f48b244b1f74d409b40b7901 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:04:14 +0000 Subject: [PATCH 2/3] Add cache invalidation test scripts Co-authored-by: rockyshi1993 <175696301+rockyshi1993@users.noreply.github.com> --- test-cache-invalidation-mock.js | 157 +++++++++++++++++ test-cache-invalidation.js | 291 ++++++++++++++++++++++++++++++++ 2 files changed, 448 insertions(+) create mode 100644 test-cache-invalidation-mock.js create mode 100644 test-cache-invalidation.js diff --git a/test-cache-invalidation-mock.js b/test-cache-invalidation-mock.js new file mode 100644 index 0000000..05f1b78 --- /dev/null +++ b/test-cache-invalidation-mock.js @@ -0,0 +1,157 @@ +/** + * Mock 版本测试:验证缓存失效逻辑(不需要 MongoDB) + * 此版本用于验证缓存失效的逻辑是否正确实现 + */ + +const MemoryCache = require('./lib/cache'); + +/** + * 等待指定的时间(毫秒) + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function testCacheLogic() { + console.log('🚀 开始测试缓存逻辑(Mock版本)\n'); + + // 创建缓存实例 + const cache = MemoryCache.createDefault({ + maxSize: 1000, + enableStats: true + }); + + try { + // ===================================================== + // 测试 1: TTL 自动过期 + // ===================================================== + console.log('=== 测试 1: TTL 自动过期 ==='); + + // 清空缓存 + await cache.clear(); + + // 第一次设置(TTL = 2 秒) + await cache.set('test:key1', { data: 'test data' }, 2000); + console.log('第一次设置缓存 (TTL = 2秒)'); + + // 立即获取(应该命中) + let value = await cache.get('test:key1'); + const stats1 = cache.stats; + console.log(`立即获取: ${value ? '缓存 HIT' : '缓存 MISS'} (hits: ${stats1.hits}, misses: ${stats1.misses})`); + + if (!value) { + throw new Error('TTL测试失败:立即获取应该命中缓存'); + } + + // 等待 2.5 秒让 TTL 过期 + console.log('等待 2.5 秒...'); + await sleep(2500); + + // 再次获取(应该过期) + value = await cache.get('test:key1'); + const stats2 = cache.stats; + console.log(`TTL过期后获取: ${value ? '缓存 HIT' : '缓存 MISS'} (hits: ${stats2.hits}, misses: ${stats2.misses})`); + + if (value) { + throw new Error('TTL测试失败:过期后不应该命中缓存'); + } + + console.log('✓ TTL 自动过期测试通过\n'); + + // ===================================================== + // 测试 2: 模式匹配删除(模拟写操作失效) + // ===================================================== + console.log('=== 测试 2: 模式匹配删除(写操作失效) ==='); + + // 清空缓存并设置多个键 + await cache.clear(); + + // 模拟同一集合的不同查询缓存 + await cache.set('test:collection:users:find:query1', { count: 2 }, 60000); + await cache.set('test:collection:users:find:query2', { count: 3 }, 60000); + await cache.set('test:collection:users:findOne:query1', { user: 'Alice' }, 60000); + await cache.set('test:collection:other:find:query1', { count: 5 }, 60000); + + console.log('设置了 4 个缓存键'); + + // 验证所有键都存在 + let exists1 = await cache.exists('test:collection:users:find:query1'); + let exists2 = await cache.exists('test:collection:users:find:query2'); + let exists3 = await cache.exists('test:collection:users:findOne:query1'); + let exists4 = await cache.exists('test:collection:other:find:query1'); + + console.log(`缓存状态: users查询1=${exists1}, users查询2=${exists2}, users查询3=${exists3}, other查询=${exists4}`); + + // 模拟写操作:删除 users 集合的所有缓存 + const deleted = await cache.delPattern('*collection:users*'); + console.log(`执行模式删除 (*collection:users*): 删除了 ${deleted} 个键`); + + // 验证只有 users 相关的缓存被删除 + exists1 = await cache.exists('test:collection:users:find:query1'); + exists2 = await cache.exists('test:collection:users:find:query2'); + exists3 = await cache.exists('test:collection:users:findOne:query1'); + exists4 = await cache.exists('test:collection:other:find:query1'); + + console.log(`删除后状态: users查询1=${exists1}, users查询2=${exists2}, users查询3=${exists3}, other查询=${exists4}`); + + if (exists1 || exists2 || exists3) { + throw new Error('模式匹配删除测试失败:users相关缓存应该被删除'); + } + + if (!exists4) { + throw new Error('模式匹配删除测试失败:other集合的缓存不应该被删除'); + } + + console.log('✓ 模式匹配删除测试通过\n'); + + // ===================================================== + // 测试 3: 缓存统计信息 + // ===================================================== + console.log('=== 测试 3: 缓存统计信息 ==='); + + // 创建新的缓存实例以获得干净的统计 + const testCache = MemoryCache.createDefault({ + maxSize: 1000, + enableStats: true + }); + + const initialStats = testCache.stats; + console.log(`初始统计: hits=${initialStats.hits}, misses=${initialStats.misses}, sets=${initialStats.sets}, deletes=${initialStats.deletes}`); + + // 执行一系列操作 + await testCache.set('key1', 'value1', 60000); // sets +1 + await testCache.set('key2', 'value2', 60000); // sets +1 + await testCache.get('key1'); // hits +1 + await testCache.get('key1'); // hits +1 + await testCache.get('key3'); // misses +1 + await testCache.del('key1'); // deletes +1 + + const finalStats = testCache.stats; + console.log(`最终统计: hits=${finalStats.hits}, misses=${finalStats.misses}, sets=${finalStats.sets}, deletes=${finalStats.deletes}`); + + // 验证统计数据(由于是新实例,直接比较绝对值) + if (finalStats.hits !== 2) { + throw new Error(`统计测试失败:期望 hits = 2,实际 = ${finalStats.hits}`); + } + if (finalStats.misses !== 1) { + throw new Error(`统计测试失败:期望 misses = 1,实际 = ${finalStats.misses}`); + } + if (finalStats.sets !== 2) { + throw new Error(`统计测试失败:期望 sets = 2,实际 = ${finalStats.sets}`); + } + if (finalStats.deletes !== 1) { + throw new Error(`统计测试失败:期望 deletes = 1,实际 = ${finalStats.deletes}`); + } + + console.log('✓ 缓存统计信息测试通过\n'); + + console.log('✅ 所有缓存逻辑测试通过!'); + console.log('\n💡 提示:运行完整的集成测试请使用 `node test-cache-invalidation.js`'); + + } catch (error) { + console.error('❌ 测试失败:', error.message); + process.exit(1); + } +} + +testCacheLogic(); diff --git a/test-cache-invalidation.js b/test-cache-invalidation.js new file mode 100644 index 0000000..dedb0ed --- /dev/null +++ b/test-cache-invalidation.js @@ -0,0 +1,291 @@ +/** + * 测试脚本:验证 monSQLize 的查询缓存自动失效功能 + * + * 运行方式: + * ```bash + * # 方式 1:使用 MongoDB Memory Server(需要网络连接) + * node test-cache-invalidation.js + * + * # 方式 2:使用本地 MongoDB 实例 + * MONGODB_URI="mongodb://localhost:27017" node test-cache-invalidation.js + * + * # 方式 3:使用远程 MongoDB 实例 + * MONGODB_URI="mongodb://username:password@host:port" node test-cache-invalidation.js + * ``` + * + * 测试内容: + * 1. TTL 自动过期测试 - 验证缓存在 TTL 到期后会自动失效 + * 2. insertOne 自动失效测试 - 验证插入操作会自动失效相关缓存 + * 3. updateOne 自动失效测试 - 验证更新操作会自动失效相关缓存 + * 4. deleteOne 自动失效测试 - 验证删除操作会自动失效相关缓存 + * + * 预期输出: + * ``` + * 🚀 开始测试缓存自动失效功能 + * + * === 测试 1: TTL 自动过期 === + * 第一次查询: 2 条记录 (缓存 MISS) + * 第二次查询: 2 条记录 (缓存 HIT) + * 等待 2.5 秒... + * 第三次查询: 2 条记录 (缓存 MISS - TTL 过期) + * ✓ TTL 自动过期测试通过 + * + * === 测试 2: insertOne 自动失效 === + * 查询前: 2 条记录 (缓存) + * 插入新记录: Charlie + * 查询后: 3 条记录 (缓存已自动失效) + * ✓ insertOne 自动失效测试通过 + * + * === 测试 3: updateOne 自动失效 === + * 更新前: Alice 的 age = 25 + * 更新 Alice 的 age 为 26 + * 更新后: Alice 的 age = 26 (缓存已自动失效) + * ✓ updateOne 自动失效测试通过 + * + * === 测试 4: deleteOne 自动失效 === + * 删除前: 3 条记录 + * 删除 Charlie + * 删除后: 2 条记录 (缓存已自动失效) + * ✓ deleteOne 自动失效测试通过 + * + * ✅ 所有测试通过! + * ``` + */ + +const MonSQLize = require('./lib/index'); + +/** + * 等待指定的时间(毫秒) + * @param {number} ms - 等待的毫秒数 + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * 获取缓存统计信息并判断是否来自缓存 + * @param {Object} prevStats - 之前的缓存统计信息 + * @param {Object} currentStats - 当前的缓存统计信息 + * @returns {string} 返回 'MISS' 或 'HIT' + */ +function getCacheStatus(prevStats, currentStats) { + if (currentStats.hits > prevStats.hits) { + return 'HIT'; + } else if (currentStats.misses > prevStats.misses) { + return 'MISS'; + } + return 'UNKNOWN'; +} + +async function testCacheInvalidation() { + console.log('🚀 开始测试缓存自动失效功能\n'); + + // 配置:优先使用环境变量 MONGODB_URI,否则使用内存数据库 + const mongoConfig = process.env.MONGODB_URI + ? { uri: process.env.MONGODB_URI } + : { useMemoryServer: true }; + + const msq = new MonSQLize({ + type: 'mongodb', + databaseName: 'test_cache', + config: mongoConfig, + cache: { + maxSize: 1000, + enableStats: true + } + }); + + try { + await msq.connect(); + const collection = msq.collection('test_users'); + + // 清理数据 + await collection.deleteMany({}); + + // 插入初始数据 + await collection.insertOne({ name: 'Alice', age: 25 }); + await collection.insertOne({ name: 'Bob', age: 30 }); + + // ===================================================== + // 测试 1: TTL 自动过期 + // ===================================================== + console.log('=== 测试 1: TTL 自动过期 ==='); + + // 清空缓存以确保干净的起点 + await msq.getCache().clear(); + + // 第一次查询(缓存 MISS,TTL = 2 秒) + let stats = msq.getCache().stats; + let prevHits = stats.hits; + let prevMisses = stats.misses; + + const result1 = await collection.find({}, { cache: 2000 }); + stats = msq.getCache().stats; + const status1 = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`第一次查询: ${result1.length} 条记录 (缓存 ${status1})`); + + // 第二次查询(立即查询,缓存 HIT) + prevHits = stats.hits; + prevMisses = stats.misses; + + const result2 = await collection.find({}, { cache: 2000 }); + stats = msq.getCache().stats; + const status2 = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`第二次查询: ${result2.length} 条记录 (缓存 ${status2})`); + + // 等待 2.5 秒让 TTL 过期 + console.log('等待 2.5 秒...'); + await sleep(2500); + + // 第三次查询(TTL 过期,缓存 MISS) + prevHits = stats.hits; + prevMisses = stats.misses; + + const result3 = await collection.find({}, { cache: 2000 }); + stats = msq.getCache().stats; + const status3 = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`第三次查询: ${result3.length} 条记录 (缓存 ${status3} - TTL 过期)`); + + if (status1 === 'MISS' && status2 === 'HIT' && status3 === 'MISS') { + console.log('✓ TTL 自动过期测试通过\n'); + } else { + throw new Error(`TTL 测试失败: 期望 MISS->HIT->MISS, 实际 ${status1}->${status2}->${status3}`); + } + + // ===================================================== + // 测试 2: insertOne 自动失效 + // ===================================================== + console.log('=== 测试 2: insertOne 自动失效 ==='); + + // 清空缓存 + await msq.getCache().clear(); + + // 查询并缓存数据 + const beforeInsert = await collection.find({}, { cache: 60000 }); + console.log(`查询前: ${beforeInsert.length} 条记录 (缓存)`); + + // 确认数据已被缓存 + prevHits = msq.getCache().stats.hits; + prevMisses = msq.getCache().stats.misses; + const cachedBeforeInsert = await collection.find({}, { cache: 60000 }); + stats = msq.getCache().stats; + const cacheStatus = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + + if (cacheStatus !== 'HIT') { + throw new Error('数据未被成功缓存'); + } + + // 执行 insertOne 操作 + console.log('插入新记录: Charlie'); + await collection.insertOne({ name: 'Charlie', age: 35 }); + + // 查询后(缓存应该已失效) + prevHits = stats.hits; + prevMisses = stats.misses; + + const afterInsert = await collection.find({}, { cache: 60000 }); + stats = msq.getCache().stats; + const afterInsertStatus = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`查询后: ${afterInsert.length} 条记录 (缓存已自动失效)`); + + if (afterInsert.length === 3 && afterInsertStatus === 'MISS') { + console.log('✓ insertOne 自动失效测试通过\n'); + } else { + throw new Error(`insertOne 测试失败: 记录数=${afterInsert.length}, 缓存状态=${afterInsertStatus}`); + } + + // ===================================================== + // 测试 3: updateOne 自动失效 + // ===================================================== + console.log('=== 测试 3: updateOne 自动失效 ==='); + + // 清空缓存 + await msq.getCache().clear(); + + // 查询 Alice 的数据 + const aliceBefore = await collection.findOne({ name: 'Alice' }, { cache: 60000 }); + console.log(`更新前: Alice 的 age = ${aliceBefore.age}`); + + // 确认数据已被缓存 + prevHits = msq.getCache().stats.hits; + prevMisses = msq.getCache().stats.misses; + await collection.findOne({ name: 'Alice' }, { cache: 60000 }); + stats = msq.getCache().stats; + const cacheStatusUpdate = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + + if (cacheStatusUpdate !== 'HIT') { + throw new Error('数据未被成功缓存'); + } + + // 更新 Alice 的 age + console.log('更新 Alice 的 age 为 26'); + await collection.updateOne({ name: 'Alice' }, { $set: { age: 26 } }); + + // 查询更新后的数据(缓存应该已失效) + prevHits = stats.hits; + prevMisses = stats.misses; + + const aliceAfter = await collection.findOne({ name: 'Alice' }, { cache: 60000 }); + stats = msq.getCache().stats; + const afterUpdateStatus = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`更新后: Alice 的 age = ${aliceAfter.age} (缓存已自动失效)`); + + if (aliceAfter.age === 26 && afterUpdateStatus === 'MISS') { + console.log('✓ updateOne 自动失效测试通过\n'); + } else { + throw new Error(`updateOne 测试失败: age=${aliceAfter.age}, 缓存状态=${afterUpdateStatus}`); + } + + // ===================================================== + // 测试 4: deleteOne 自动失效 + // ===================================================== + console.log('=== 测试 4: deleteOne 自动失效 ==='); + + // 清空缓存 + await msq.getCache().clear(); + + // 查询所有数据 + const beforeDelete = await collection.find({}, { cache: 60000 }); + console.log(`删除前: ${beforeDelete.length} 条记录`); + + // 确认数据已被缓存 + prevHits = msq.getCache().stats.hits; + prevMisses = msq.getCache().stats.misses; + await collection.find({}, { cache: 60000 }); + stats = msq.getCache().stats; + const cacheStatusDelete = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + + if (cacheStatusDelete !== 'HIT') { + throw new Error('数据未被成功缓存'); + } + + // 删除 Charlie + console.log('删除 Charlie'); + await collection.deleteOne({ name: 'Charlie' }); + + // 查询删除后的数据(缓存应该已失效) + prevHits = stats.hits; + prevMisses = stats.misses; + + const afterDelete = await collection.find({}, { cache: 60000 }); + stats = msq.getCache().stats; + const afterDeleteStatus = getCacheStatus({ hits: prevHits, misses: prevMisses }, stats); + console.log(`删除后: ${afterDelete.length} 条记录 (缓存已自动失效)`); + + if (afterDelete.length === 2 && afterDeleteStatus === 'MISS') { + console.log('✓ deleteOne 自动失效测试通过\n'); + } else { + throw new Error(`deleteOne 测试失败: 记录数=${afterDelete.length}, 缓存状态=${afterDeleteStatus}`); + } + + console.log('✅ 所有测试通过!'); + + } catch (error) { + console.error('❌ 测试失败:', error); + process.exit(1); + } finally { + await msq.close(); + } +} + +testCacheInvalidation(); From 426a4508b2dccc08cc039911420ffb9e6d11f869 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:04:55 +0000 Subject: [PATCH 3/3] Add documentation for cache invalidation tests Co-authored-by: rockyshi1993 <175696301+rockyshi1993@users.noreply.github.com> --- TEST-CACHE-README.md | 182 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 TEST-CACHE-README.md diff --git a/TEST-CACHE-README.md b/TEST-CACHE-README.md new file mode 100644 index 0000000..58bfc78 --- /dev/null +++ b/TEST-CACHE-README.md @@ -0,0 +1,182 @@ +# 缓存自动失效功能测试 + +本目录包含用于测试 monSQLize 查询缓存自动失效功能的测试脚本。 + +## 测试脚本 + +### 1. test-cache-invalidation.js(完整集成测试) + +完整的集成测试,验证缓存在实际 MongoDB 环境中的自动失效功能。 + +**测试内容:** +- ✅ TTL 自动过期 - 验证缓存在 TTL 到期后会自动失效 +- ✅ insertOne 自动失效 - 验证插入操作会自动失效相关缓存 +- ✅ updateOne 自动失效 - 验证更新操作会自动失效相关缓存 +- ✅ deleteOne 自动失效 - 验证删除操作会自动失效相关缓存 + +**运行方式:** + +```bash +# 方式 1:使用 MongoDB Memory Server(需要网络连接) +node test-cache-invalidation.js + +# 方式 2:使用本地 MongoDB 实例 +MONGODB_URI="mongodb://localhost:27017" node test-cache-invalidation.js + +# 方式 3:使用远程 MongoDB 实例 +MONGODB_URI="mongodb://username:password@host:port" node test-cache-invalidation.js +``` + +**注意事项:** +- MongoDB Memory Server 需要网络连接来下载 MongoDB 二进制文件 +- 如果在没有网络的环境中,请使用本地或远程 MongoDB 实例 + +### 2. test-cache-invalidation-mock.js(Mock 测试) + +基于 Mock 的单元测试,不需要 MongoDB 实例,用于验证缓存失效的核心逻辑。 + +**测试内容:** +- ✅ TTL 自动过期逻辑 +- ✅ 模式匹配删除(模拟写操作失效) +- ✅ 缓存统计信息 + +**运行方式:** + +```bash +node test-cache-invalidation-mock.js +``` + +**优点:** +- 无需 MongoDB 实例 +- 运行速度快 +- 可以在任何环境中运行 + +## 预期输出 + +### test-cache-invalidation.js 预期输出: + +``` +🚀 开始测试缓存自动失效功能 + +=== 测试 1: TTL 自动过期 === +第一次查询: 2 条记录 (缓存 MISS) +第二次查询: 2 条记录 (缓存 HIT) +等待 2.5 秒... +第三次查询: 2 条记录 (缓存 MISS - TTL 过期) +✓ TTL 自动过期测试通过 + +=== 测试 2: insertOne 自动失效 === +查询前: 2 条记录 (缓存) +插入新记录: Charlie +查询后: 3 条记录 (缓存已自动失效) +✓ insertOne 自动失效测试通过 + +=== 测试 3: updateOne 自动失效 === +更新前: Alice 的 age = 25 +更新 Alice 的 age 为 26 +更新后: Alice 的 age = 26 (缓存已自动失效) +✓ updateOne 自动失效测试通过 + +=== 测试 4: deleteOne 自动失效 === +删除前: 3 条记录 +删除 Charlie +删除后: 2 条记录 (缓存已自动失效) +✓ deleteOne 自动失效测试通过 + +✅ 所有测试通过! +``` + +### test-cache-invalidation-mock.js 预期输出: + +``` +🚀 开始测试缓存逻辑(Mock版本) + +=== 测试 1: TTL 自动过期 === +第一次设置缓存 (TTL = 2秒) +立即获取: 缓存 HIT (hits: 1, misses: 0) +等待 2.5 秒... +TTL过期后获取: 缓存 MISS (hits: 1, misses: 1) +✓ TTL 自动过期测试通过 + +=== 测试 2: 模式匹配删除(写操作失效) === +设置了 4 个缓存键 +缓存状态: users查询1=true, users查询2=true, users查询3=true, other查询=true +执行模式删除 (*collection:users*): 删除了 3 个键 +删除后状态: users查询1=false, users查询2=false, users查询3=false, other查询=true +✓ 模式匹配删除测试通过 + +=== 测试 3: 缓存统计信息 === +初始统计: hits=0, misses=0, sets=0, deletes=0 +最终统计: hits=2, misses=1, sets=2, deletes=1 +✓ 缓存统计信息测试通过 + +✅ 所有缓存逻辑测试通过! + +💡 提示:运行完整的集成测试请使用 `node test-cache-invalidation.js` +``` + +## 技术实现 + +### TTL 自动过期 + +monSQLize 的缓存系统使用惰性过期策略: +- 缓存条目存储 `expireAt` 时间戳 +- 在 `get()` 操作时检查是否过期 +- 过期的条目会被自动删除并返回 `undefined` + +### 写操作自动失效 + +写操作(insertOne、updateOne、deleteOne)会自动失效相关缓存: +1. 写操作完成后,调用 `cache.delPattern()` 删除匹配的缓存键 +2. 使用命名空间模式匹配:`*{iid}:{type}:{db}:{collection}*` +3. 删除该集合的所有查询缓存(find、findOne、count 等) + +### 缓存统计 + +缓存实例维护详细的统计信息: +- `hits` - 缓存命中次数 +- `misses` - 缓存未命中次数 +- `sets` - 设置缓存次数 +- `deletes` - 删除缓存次数 +- `evictions` - 淘汰次数(LRU) +- `memoryUsage` - 内存使用量(估算) + +## 故障排除 + +### MongoDB Memory Server 下载失败 + +**错误信息:** +``` +Could NOT download "https://fastdl.mongodb.org/..." +getaddrinfo ENOTFOUND fastdl.mongodb.org +``` + +**解决方案:** +1. 使用本地 MongoDB 实例: + ```bash + # 启动本地 MongoDB + mongod + + # 运行测试 + MONGODB_URI="mongodb://localhost:27017" node test-cache-invalidation.js + ``` + +2. 或者运行 Mock 测试: + ```bash + node test-cache-invalidation-mock.js + ``` + +### 测试超时 + +如果测试因超时失败,可能是因为: +- MongoDB 连接速度慢 +- Memory Server 下载速度慢 + +**解决方案:** +- 使用本地 MongoDB 实例 +- 增加超时时间(修改测试代码) + +## 相关文档 + +- [monSQLize 缓存文档](../docs/cache.md) +- [monSQLize API 文档](../README.md)