Skip to content

Commit e798a4a

Browse files
committed
feat:v1.0.8
1 parent 3162a30 commit e798a4a

7 files changed

Lines changed: 286 additions & 5 deletions

File tree

lib/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,12 +889,14 @@ module.exports = class {
889889
// 停止 Change Stream 同步
890890
if (this._syncManager) {
891891
await this._syncManager.stop();
892+
this._syncManager = null;
892893
this.logger.info('[MonSQLize] Change Stream 同步已停止');
893894
}
894895

895896
// 关闭多连接池
896897
if (this._poolManager) {
897898
await this._poolManager.close();
899+
this._poolManager = null;
898900
this.logger.info('[MonSQLize] 连接池已关闭');
899901
}
900902

@@ -904,7 +906,12 @@ module.exports = class {
904906
this.logger.info('[MonSQLize] 数据库连接已关闭');
905907
}
906908

909+
// 清理所有引用,防止内存泄漏
910+
this._adapter = null;
907911
this.dbInstance = null;
912+
this._connecting = null;
913+
914+
return null;
908915
}
909916
};
910917

lib/sync/ChangeStreamSyncManager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* - 错误处理和自动重连
1111
*
1212
* @module lib/sync/ChangeStreamSyncManager
13-
* @since v1.0.9
13+
* @since v1.0.8
1414
*/
1515

1616
const SyncTarget = require('./SyncTarget');

lib/sync/ResumeTokenStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 支持文件和 Redis 两种存储方式
66
*
77
* @module lib/sync/ResumeTokenStore
8-
* @since v1.0.9
8+
* @since v1.0.8
99
*/
1010

1111
const fs = require('fs').promises;

lib/sync/SyncConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* 负责验证和规范化 Change Stream 同步配置
55
*
66
* @module lib/sync/SyncConfig
7-
* @since v1.0.9
7+
* @since v1.0.8
88
*/
99

1010
/**

lib/sync/SyncTarget.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 复用 ConnectionPoolManager 管理连接
66
*
77
* @module lib/sync/SyncTarget
8-
* @since v1.0.9
8+
* @since v1.0.8
99
*/
1010

1111
/**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
"dependencies": {
114114
"async-lock": "^1.4.1",
115115
"mongodb": "^6.17.0",
116-
"schema-dsl": "^1.1.3",
116+
"schema-dsl": "^1.1.5",
117117
"ssh2": "^1.17.0"
118118
}
119119
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/**
2+
* ResumeTokenStore 错误注入测试
3+
*
4+
* 测试错误处理和边缘情况,提升覆盖率到 95%+
5+
*/
6+
7+
const { expect } = require('chai');
8+
const sinon = require('sinon');
9+
const fs = require('fs').promises;
10+
const path = require('path');
11+
const ResumeTokenStore = require('../../../lib/sync/ResumeTokenStore');
12+
13+
describe('ResumeTokenStore 错误注入测试', () => {
14+
15+
const testTokenPath = path.join(__dirname, '.test-resume-token-error');
16+
const testToken = { _data: 'test-token-data', timestamp: Date.now() };
17+
18+
afterEach(async () => {
19+
// 清理测试文件
20+
try {
21+
await fs.unlink(testTokenPath);
22+
} catch (error) {
23+
// 忽略
24+
}
25+
// 恢复所有 stub
26+
sinon.restore();
27+
});
28+
29+
describe('文件系统错误处理', () => {
30+
31+
it('应该处理文件读取权限错误', async () => {
32+
const store = new ResumeTokenStore({
33+
storage: 'file',
34+
path: testTokenPath
35+
});
36+
37+
// 先创建文件
38+
await store.save(testToken);
39+
40+
// Mock fs.readFile 抛出权限错误
41+
const readFileStub = sinon.stub(fs, 'readFile');
42+
readFileStub.rejects(new Error('EACCES: permission denied'));
43+
44+
// 应该捕获错误并返回 null
45+
const result = await store.load();
46+
47+
expect(result).to.be.null;
48+
expect(readFileStub.calledOnce).to.be.true;
49+
});
50+
51+
it('应该处理文件读取磁盘错误', async () => {
52+
const store = new ResumeTokenStore({
53+
storage: 'file',
54+
path: testTokenPath
55+
});
56+
57+
// Mock fs.readFile 抛出磁盘错误
58+
const readFileStub = sinon.stub(fs, 'readFile');
59+
readFileStub.rejects(new Error('EIO: i/o error'));
60+
61+
// 应该捕获错误并返回 null
62+
const result = await store.load();
63+
64+
expect(result).to.be.null;
65+
expect(readFileStub.calledOnce).to.be.true;
66+
});
67+
68+
it('应该处理 JSON 解析错误', async () => {
69+
const store = new ResumeTokenStore({
70+
storage: 'file',
71+
path: testTokenPath
72+
});
73+
74+
// 写入无效的 JSON
75+
await fs.writeFile(testTokenPath, 'invalid json{');
76+
77+
// 应该捕获错误并返回 null
78+
const result = await store.load();
79+
80+
expect(result).to.be.null;
81+
});
82+
83+
it('应该处理文件删除失败', async () => {
84+
const store = new ResumeTokenStore({
85+
storage: 'file',
86+
path: testTokenPath
87+
});
88+
89+
// 先保存
90+
await store.save(testToken);
91+
92+
// Mock fs.unlink 抛出错误
93+
const unlinkStub = sinon.stub(fs, 'unlink');
94+
unlinkStub.rejects(new Error('EBUSY: resource busy'));
95+
96+
// clear 应该捕获错误但不抛出
97+
await store.clear();
98+
99+
expect(unlinkStub.calledOnce).to.be.true;
100+
});
101+
102+
it('应该处理文件写入错误', async () => {
103+
const store = new ResumeTokenStore({
104+
storage: 'file',
105+
path: testTokenPath
106+
});
107+
108+
// Mock fs.writeFile 抛出错误
109+
const writeFileStub = sinon.stub(fs, 'writeFile');
110+
writeFileStub.rejects(new Error('ENOSPC: no space left'));
111+
112+
// save() 会捕获错误并记录日志,不抛出
113+
await store.save(testToken);
114+
115+
// 验证 writeFile 被调用
116+
expect(writeFileStub.calledOnce).to.be.true;
117+
});
118+
});
119+
120+
describe('Redis 错误处理', () => {
121+
122+
let mockRedis;
123+
124+
beforeEach(() => {
125+
mockRedis = {
126+
get: sinon.stub(),
127+
set: sinon.stub(),
128+
del: sinon.stub()
129+
};
130+
});
131+
132+
it('应该处理 Redis get 错误', async () => {
133+
const store = new ResumeTokenStore({
134+
storage: 'redis',
135+
redis: mockRedis
136+
});
137+
138+
// Mock Redis get 抛出错误
139+
mockRedis.get.rejects(new Error('Redis connection lost'));
140+
141+
// 应该捕获错误并返回 null
142+
const result = await store.load();
143+
144+
expect(result).to.be.null;
145+
expect(mockRedis.get.calledOnce).to.be.true;
146+
});
147+
148+
it('应该处理 Redis set 错误', async () => {
149+
const store = new ResumeTokenStore({
150+
storage: 'redis',
151+
redis: mockRedis
152+
});
153+
154+
// Mock Redis set 抛出错误
155+
mockRedis.set.rejects(new Error('Redis timeout'));
156+
157+
// save() 会捕获错误并记录日志,不抛出
158+
await store.save(testToken);
159+
160+
// 验证 set 被调用
161+
expect(mockRedis.set.calledOnce).to.be.true;
162+
});
163+
164+
it('应该处理 Redis del 错误', async () => {
165+
const store = new ResumeTokenStore({
166+
storage: 'redis',
167+
redis: mockRedis
168+
});
169+
170+
// Mock Redis del 抛出错误
171+
mockRedis.del.rejects(new Error('Redis error'));
172+
173+
// clear 应该捕获错误但不抛出
174+
await store.clear();
175+
176+
expect(mockRedis.del.calledOnce).to.be.true;
177+
});
178+
179+
it('应该处理 Redis 返回的无效数据', async () => {
180+
const store = new ResumeTokenStore({
181+
storage: 'redis',
182+
redis: mockRedis
183+
});
184+
185+
// Mock Redis 返回无效 JSON
186+
mockRedis.get.resolves('invalid json{');
187+
188+
// 应该捕获错误并返回 null
189+
const result = await store.load();
190+
191+
expect(result).to.be.null;
192+
expect(mockRedis.get.calledOnce).to.be.true;
193+
});
194+
});
195+
196+
describe('边缘情况', () => {
197+
198+
it('应该处理 Token 为 null', async () => {
199+
const store = new ResumeTokenStore({
200+
storage: 'file',
201+
path: testTokenPath
202+
});
203+
204+
// 保存 null
205+
await store.save(null);
206+
207+
// 应该能保存和读取
208+
const result = await store.load();
209+
expect(result).to.be.null;
210+
});
211+
212+
it('应该处理 Token 为空对象', async () => {
213+
const store = new ResumeTokenStore({
214+
storage: 'file',
215+
path: testTokenPath
216+
});
217+
218+
// 保存空对象
219+
await store.save({});
220+
221+
// 应该能保存和读取
222+
const result = await store.load();
223+
expect(result).to.deep.equal({});
224+
});
225+
});
226+
227+
describe('并发场景', () => {
228+
229+
it('应该处理并发读取', async () => {
230+
const store = new ResumeTokenStore({
231+
storage: 'file',
232+
path: testTokenPath
233+
});
234+
235+
// 先保存
236+
await store.save(testToken);
237+
238+
// 并发读取多次
239+
const results = await Promise.all([
240+
store.load(),
241+
store.load(),
242+
store.load(),
243+
store.load(),
244+
store.load()
245+
]);
246+
247+
// 所有结果应该一致
248+
results.forEach(result => {
249+
expect(result).to.deep.equal(testToken);
250+
});
251+
});
252+
253+
it('应该处理并发写入', async () => {
254+
const store = new ResumeTokenStore({
255+
storage: 'file',
256+
path: testTokenPath
257+
});
258+
259+
const tokens = Array.from({ length: 5 }, (_, i) => ({
260+
_data: `token-${i}`,
261+
timestamp: Date.now() + i
262+
}));
263+
264+
// 并发写入多次
265+
await Promise.all(tokens.map(token => store.save(token)));
266+
267+
// 应该能读取(最后一个写入的)
268+
const result = await store.load();
269+
expect(result).to.be.an('object');
270+
expect(result._data).to.match(/token-\d/);
271+
});
272+
});
273+
});
274+

0 commit comments

Comments
 (0)