@taicode/common-server 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output/index.d.ts +1 -0
- package/output/index.d.ts.map +1 -1
- package/output/index.js +1 -0
- package/output/redis-queue/index.d.ts +6 -4
- package/output/redis-queue/index.d.ts.map +1 -1
- package/output/redis-queue/index.js +4 -2
- package/output/redis-queue/redis-batch-consumer.d.ts +80 -0
- package/output/redis-queue/redis-batch-consumer.d.ts.map +1 -0
- package/output/redis-queue/redis-batch-consumer.js +308 -0
- package/output/redis-queue/redis-batch-consumer.test.d.ts +7 -0
- package/output/redis-queue/redis-batch-consumer.test.d.ts.map +1 -0
- package/output/redis-queue/redis-batch-consumer.test.js +265 -0
- package/output/redis-queue/redis-queue-common.d.ts +73 -0
- package/output/redis-queue/redis-queue-common.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-common.js +302 -0
- package/output/redis-queue/redis-queue-common.test.d.ts +19 -0
- package/output/redis-queue/redis-queue-common.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-common.test.js +623 -0
- package/output/redis-queue/redis-queue-consumer.d.ts +81 -0
- package/output/redis-queue/redis-queue-consumer.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-consumer.js +297 -0
- package/output/redis-queue/redis-queue-consumer.test.d.ts +7 -0
- package/output/redis-queue/redis-queue-consumer.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-consumer.test.js +242 -0
- package/output/redis-queue/redis-queue-provider.d.ts +56 -0
- package/output/redis-queue/redis-queue-provider.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-provider.js +187 -0
- package/output/redis-queue/redis-queue-provider.test.d.ts +7 -0
- package/output/redis-queue/redis-queue-provider.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-provider.test.js +114 -0
- package/output/redis-queue/types.d.ts +77 -19
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/output/logger/logger.d.ts +0 -33
- package/output/logger/logger.d.ts.map +0 -1
- package/output/logger/logger.js +0 -65
- package/output/logger/logger.test.d.ts +0 -2
- package/output/logger/logger.test.d.ts.map +0 -1
- package/output/logger/logger.test.js +0 -87
- package/output/redis-queue/batch-redis-queue.d.ts +0 -136
- package/output/redis-queue/batch-redis-queue.d.ts.map +0 -1
- package/output/redis-queue/batch-redis-queue.js +0 -573
- package/output/redis-queue/batch-redis-queue.test.d.ts +0 -2
- package/output/redis-queue/batch-redis-queue.test.d.ts.map +0 -1
- package/output/redis-queue/batch-redis-queue.test.js +0 -243
- package/output/redis-queue/redis-queue.d.ts +0 -129
- package/output/redis-queue/redis-queue.d.ts.map +0 -1
- package/output/redis-queue/redis-queue.js +0 -547
- package/output/redis-queue/redis-queue.test.d.ts +0 -2
- package/output/redis-queue/redis-queue.test.d.ts.map +0 -1
- package/output/redis-queue/redis-queue.test.js +0 -234
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { RedisQueueCommon } from './redis-queue-common';
|
|
3
|
+
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
4
|
+
// 创建一个具体的测试类来测试抽象基类
|
|
5
|
+
class TestRedisQueueCommon extends RedisQueueCommon {
|
|
6
|
+
getLogPrefix() {
|
|
7
|
+
return 'TestRedisQueue';
|
|
8
|
+
}
|
|
9
|
+
// 暴露 protected 方法用于测试
|
|
10
|
+
testGetTask(taskId) {
|
|
11
|
+
return this.getTask(taskId);
|
|
12
|
+
}
|
|
13
|
+
testGetQueueByStatus(status) {
|
|
14
|
+
return this.getQueueByStatus(status);
|
|
15
|
+
}
|
|
16
|
+
async testApplyStatus(taskId, oldStatus, newStatus) {
|
|
17
|
+
return this.applyStatus(taskId, oldStatus, newStatus);
|
|
18
|
+
}
|
|
19
|
+
getRedisClient() {
|
|
20
|
+
return this.redis;
|
|
21
|
+
}
|
|
22
|
+
getConfig() {
|
|
23
|
+
return {
|
|
24
|
+
queueKey: this.queueKey,
|
|
25
|
+
redisUrl: this.redisUrl,
|
|
26
|
+
cleanupDelay: this.cleanupDelay,
|
|
27
|
+
pendingQueue: this.pendingQueue,
|
|
28
|
+
processingQueue: this.processingQueue,
|
|
29
|
+
completedQueue: this.completedQueue,
|
|
30
|
+
failedQueue: this.failedQueue,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
describe('RedisQueueCommon', () => {
|
|
35
|
+
let instance;
|
|
36
|
+
const queueKey = `test:common:${Date.now()}:${Math.random()}`;
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
instance = new TestRedisQueueCommon({
|
|
39
|
+
redisUrl: REDIS_URL,
|
|
40
|
+
queueKey,
|
|
41
|
+
cleanupDelay: 3600,
|
|
42
|
+
});
|
|
43
|
+
await instance.connect();
|
|
44
|
+
});
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
// 清理测试数据
|
|
47
|
+
if (instance['redis'] && instance['redis'].isOpen) {
|
|
48
|
+
const allKeys = await instance['redis'].keys(`${queueKey}:*`);
|
|
49
|
+
if (allKeys.length > 0) {
|
|
50
|
+
await instance['redis'].del(allKeys);
|
|
51
|
+
}
|
|
52
|
+
await instance['redis'].del([
|
|
53
|
+
`${queueKey}:pending`,
|
|
54
|
+
`${queueKey}:processing`,
|
|
55
|
+
`${queueKey}:completed`,
|
|
56
|
+
`${queueKey}:failed`,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
if (instance['redis']?.isOpen) {
|
|
60
|
+
instance.disconnect();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
describe('连接管理', () => {
|
|
64
|
+
it('应该成功连接到 Redis', async () => {
|
|
65
|
+
expect(instance['redis']).toBeDefined();
|
|
66
|
+
expect(instance['redis']?.isOpen).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('应该成功断开连接', async () => {
|
|
69
|
+
instance.disconnect();
|
|
70
|
+
expect(instance['redis']?.isOpen).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('应该初始化正确的队列键名', () => {
|
|
73
|
+
expect(instance['pendingQueue']).toBe(`${queueKey}:pending`);
|
|
74
|
+
expect(instance['processingQueue']).toBe(`${queueKey}:processing`);
|
|
75
|
+
expect(instance['completedQueue']).toBe(`${queueKey}:completed`);
|
|
76
|
+
expect(instance['failedQueue']).toBe(`${queueKey}:failed`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('getTask', () => {
|
|
80
|
+
it('应该能获取存在的任务', async () => {
|
|
81
|
+
const taskId = 'test-task-1';
|
|
82
|
+
const task = {
|
|
83
|
+
id: taskId,
|
|
84
|
+
data: { message: 'test' },
|
|
85
|
+
retryCount: 0,
|
|
86
|
+
maxRetries: 3,
|
|
87
|
+
createdTime: new Date().toISOString(),
|
|
88
|
+
status: 'pending',
|
|
89
|
+
};
|
|
90
|
+
// 手动存储任务
|
|
91
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
92
|
+
await instance['redis'].setEx(taskKey, 3600, JSON.stringify(task));
|
|
93
|
+
const result = await instance['getTask'](taskId);
|
|
94
|
+
expect(result).toEqual(task);
|
|
95
|
+
});
|
|
96
|
+
it('应该在任务不存在时返回 null', async () => {
|
|
97
|
+
const result = await instance['getTask']('non-existent-task');
|
|
98
|
+
expect(result).toBe(null);
|
|
99
|
+
});
|
|
100
|
+
it('应该在 Redis 未连接时返回 null', async () => {
|
|
101
|
+
instance.disconnect();
|
|
102
|
+
// 等待断开连接完成
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
104
|
+
const result = await instance['getTask']('any-task');
|
|
105
|
+
expect(result).toBe(null);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('applyStatus', () => {
|
|
109
|
+
const taskId = 'test-task-status';
|
|
110
|
+
beforeEach(async () => {
|
|
111
|
+
// 创建测试任务
|
|
112
|
+
const task = {
|
|
113
|
+
id: taskId,
|
|
114
|
+
data: { message: 'test' },
|
|
115
|
+
retryCount: 0,
|
|
116
|
+
maxRetries: 3,
|
|
117
|
+
createdTime: new Date().toISOString(),
|
|
118
|
+
status: 'pending',
|
|
119
|
+
};
|
|
120
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
121
|
+
await instance['redis'].setEx(taskKey, 3600, JSON.stringify(task));
|
|
122
|
+
await instance['redis'].rPush(instance['pendingQueue'], taskId);
|
|
123
|
+
});
|
|
124
|
+
it('应该能将任务状态更新为 processing', async () => {
|
|
125
|
+
await instance['applyStatus'](taskId, 'pending', 'processing');
|
|
126
|
+
const task = await instance['getTask'](taskId);
|
|
127
|
+
expect(task?.status).toBe('processing');
|
|
128
|
+
// 验证任务在正确的队列中
|
|
129
|
+
const processingTasks = await instance['redis'].lRange(instance['processingQueue'], 0, -1);
|
|
130
|
+
expect(processingTasks).toContain(taskId);
|
|
131
|
+
});
|
|
132
|
+
it('应该能将任务状态更新为 completed', async () => {
|
|
133
|
+
await instance['applyStatus'](taskId, 'pending', 'completed');
|
|
134
|
+
const task = await instance['getTask'](taskId);
|
|
135
|
+
expect(task?.status).toBe('completed');
|
|
136
|
+
const completedTasks = await instance['redis'].lRange(instance['completedQueue'], 0, -1);
|
|
137
|
+
expect(completedTasks).toContain(taskId);
|
|
138
|
+
});
|
|
139
|
+
it('应该能将任务状态更新为 failed', async () => {
|
|
140
|
+
await instance['applyStatus'](taskId, 'pending', 'failed');
|
|
141
|
+
const task = await instance['getTask'](taskId);
|
|
142
|
+
expect(task?.status).toBe('failed');
|
|
143
|
+
const failedTasks = await instance['redis'].lRange(instance['failedQueue'], 0, -1);
|
|
144
|
+
expect(failedTasks).toContain(taskId);
|
|
145
|
+
});
|
|
146
|
+
it('应该在 Redis 未连接时不执行任何操作', async () => {
|
|
147
|
+
instance.disconnect();
|
|
148
|
+
// 等待断开连接完成
|
|
149
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
150
|
+
await instance['applyStatus'](taskId, 'pending', 'completed');
|
|
151
|
+
// 不应该抛出错误
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('getQueueByStatus', () => {
|
|
155
|
+
it('应该返回 pending 队列的键名', () => {
|
|
156
|
+
const queueKey = instance['getQueueByStatus']('pending');
|
|
157
|
+
expect(queueKey).toBe(`${instance['queueKey']}:pending`);
|
|
158
|
+
});
|
|
159
|
+
it('应该返回 processing 队列的键名', () => {
|
|
160
|
+
const queueKey = instance['getQueueByStatus']('processing');
|
|
161
|
+
expect(queueKey).toBe(`${instance['queueKey']}:processing`);
|
|
162
|
+
});
|
|
163
|
+
it('应该返回 completed 队列的键名', () => {
|
|
164
|
+
const queueKey = instance['getQueueByStatus']('completed');
|
|
165
|
+
expect(queueKey).toBe(`${instance['queueKey']}:completed`);
|
|
166
|
+
});
|
|
167
|
+
it('应该返回 failed 队列的键名', () => {
|
|
168
|
+
const queueKey = instance['getQueueByStatus']('failed');
|
|
169
|
+
expect(queueKey).toBe(`${instance['queueKey']}:failed`);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('statistics', () => {
|
|
173
|
+
it('应该返回正确的队列统计信息', async () => {
|
|
174
|
+
// 创建不同状态的任务
|
|
175
|
+
const taskIds = {
|
|
176
|
+
pending: ['p1', 'p2'],
|
|
177
|
+
processing: ['pr1'],
|
|
178
|
+
completed: ['c1', 'c2', 'c3'],
|
|
179
|
+
failed: ['f1'],
|
|
180
|
+
};
|
|
181
|
+
// 添加任务到各个队列
|
|
182
|
+
for (const [status, ids] of Object.entries(taskIds)) {
|
|
183
|
+
const queueName = `${queueKey}:${status}`;
|
|
184
|
+
for (const id of ids) {
|
|
185
|
+
await instance['redis'].rPush(queueName, id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const stats = await instance.statistics();
|
|
189
|
+
expect(stats).toEqual({
|
|
190
|
+
pending: 2,
|
|
191
|
+
processing: 1,
|
|
192
|
+
completed: 3,
|
|
193
|
+
failed: 1,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
it('应该在所有队列为空时返回全零统计', async () => {
|
|
197
|
+
const stats = await instance.statistics();
|
|
198
|
+
expect(stats).toEqual({
|
|
199
|
+
pending: 0,
|
|
200
|
+
processing: 0,
|
|
201
|
+
completed: 0,
|
|
202
|
+
failed: 0,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('应该在 Redis 未连接时返回全零统计', async () => {
|
|
206
|
+
instance.disconnect();
|
|
207
|
+
// 等待断开连接完成
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
209
|
+
const stats = await instance.statistics();
|
|
210
|
+
expect(stats).toEqual({
|
|
211
|
+
pending: 0,
|
|
212
|
+
processing: 0,
|
|
213
|
+
completed: 0,
|
|
214
|
+
failed: 0,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('health', () => {
|
|
219
|
+
it('应该在 Redis 连接正常时返回 true', async () => {
|
|
220
|
+
const health = await instance.health();
|
|
221
|
+
expect(health).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it('应该在 Redis 未连接时返回 false', async () => {
|
|
224
|
+
instance.disconnect();
|
|
225
|
+
// 等待断开连接完成
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
227
|
+
const health = await instance.health();
|
|
228
|
+
expect(health).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('队列键名验证', () => {
|
|
232
|
+
it('应该拒绝长度小于6的 queueKey', () => {
|
|
233
|
+
expect(() => {
|
|
234
|
+
new TestRedisQueueCommon({
|
|
235
|
+
redisUrl: REDIS_URL,
|
|
236
|
+
queueKey: 'short',
|
|
237
|
+
cleanupDelay: 3600,
|
|
238
|
+
});
|
|
239
|
+
}).toThrow('queueKey must be at least 6 characters long');
|
|
240
|
+
});
|
|
241
|
+
it('应该接受长度大于等于6的 queueKey', () => {
|
|
242
|
+
expect(() => {
|
|
243
|
+
new TestRedisQueueCommon({
|
|
244
|
+
redisUrl: REDIS_URL,
|
|
245
|
+
queueKey: 'valid-key',
|
|
246
|
+
cleanupDelay: 3600,
|
|
247
|
+
});
|
|
248
|
+
}).not.toThrow();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
describe('cleanupDelay 配置', () => {
|
|
252
|
+
it('应该使用默认的 cleanupDelay', () => {
|
|
253
|
+
const defaultInstance = new TestRedisQueueCommon({
|
|
254
|
+
redisUrl: REDIS_URL,
|
|
255
|
+
queueKey: 'test-default',
|
|
256
|
+
});
|
|
257
|
+
expect(defaultInstance['cleanupDelay']).toBe(86400); // 默认 24 小时
|
|
258
|
+
});
|
|
259
|
+
it('应该使用自定义的 cleanupDelay', () => {
|
|
260
|
+
const customInstance = new TestRedisQueueCommon({
|
|
261
|
+
redisUrl: REDIS_URL,
|
|
262
|
+
queueKey: 'test-custom',
|
|
263
|
+
cleanupDelay: 7200,
|
|
264
|
+
});
|
|
265
|
+
expect(customInstance['cleanupDelay']).toBe(7200);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('构造函数参数验证', () => {
|
|
269
|
+
it('应该拒绝空的 redisUrl 和 redisClient', () => {
|
|
270
|
+
expect(() => {
|
|
271
|
+
new TestRedisQueueCommon({
|
|
272
|
+
redisUrl: '',
|
|
273
|
+
queueKey: 'test-queue',
|
|
274
|
+
});
|
|
275
|
+
}).toThrow('Either redisUrl or redisClient is required');
|
|
276
|
+
});
|
|
277
|
+
it('应该拒绝空的 queueKey', () => {
|
|
278
|
+
expect(() => {
|
|
279
|
+
new TestRedisQueueCommon({
|
|
280
|
+
redisUrl: REDIS_URL,
|
|
281
|
+
queueKey: '',
|
|
282
|
+
});
|
|
283
|
+
}).toThrow('queueKey is required');
|
|
284
|
+
});
|
|
285
|
+
it('应该正确初始化所有队列键名', () => {
|
|
286
|
+
const testQueue = new TestRedisQueueCommon({
|
|
287
|
+
redisUrl: REDIS_URL,
|
|
288
|
+
queueKey: 'my-queue',
|
|
289
|
+
});
|
|
290
|
+
const config = testQueue.getConfig();
|
|
291
|
+
expect(config.pendingQueue).toBe('my-queue:pending');
|
|
292
|
+
expect(config.processingQueue).toBe('my-queue:processing');
|
|
293
|
+
expect(config.completedQueue).toBe('my-queue:completed');
|
|
294
|
+
expect(config.failedQueue).toBe('my-queue:failed');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe('连接状态管理', () => {
|
|
298
|
+
it('应该能够重复连接', async () => {
|
|
299
|
+
await instance.connect();
|
|
300
|
+
await instance.connect();
|
|
301
|
+
expect(instance.getRedisClient()?.isOpen).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
it('应该能够在断开后重新连接', async () => {
|
|
304
|
+
instance.disconnect();
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
306
|
+
const newInstance = new TestRedisQueueCommon({
|
|
307
|
+
redisUrl: REDIS_URL,
|
|
308
|
+
queueKey: `test:reconnect:${Date.now()}`,
|
|
309
|
+
});
|
|
310
|
+
await newInstance.connect();
|
|
311
|
+
expect(newInstance.getRedisClient()?.isOpen).toBe(true);
|
|
312
|
+
newInstance.disconnect();
|
|
313
|
+
});
|
|
314
|
+
it('应该正确处理 Redis 错误事件', async () => {
|
|
315
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
316
|
+
// 创建一个带有错误的实例
|
|
317
|
+
const errorInstance = new TestRedisQueueCommon({
|
|
318
|
+
redisUrl: REDIS_URL,
|
|
319
|
+
queueKey: 'test-error',
|
|
320
|
+
});
|
|
321
|
+
// 模拟 Redis 错误
|
|
322
|
+
const redis = errorInstance.getRedisClient();
|
|
323
|
+
if (redis) {
|
|
324
|
+
redis.emit('error', new Error('Test error'));
|
|
325
|
+
}
|
|
326
|
+
// 验证错误被记录
|
|
327
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
328
|
+
errorSpy.mockRestore();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
describe('getTask 边界测试', () => {
|
|
332
|
+
it('应该处理不存在的任务ID', async () => {
|
|
333
|
+
const task = await instance.testGetTask('non-existent-id');
|
|
334
|
+
expect(task).toBeNull();
|
|
335
|
+
});
|
|
336
|
+
it('应该处理无效的JSON数据', async () => {
|
|
337
|
+
const taskId = 'invalid-json-task';
|
|
338
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
339
|
+
// 直接写入无效的JSON
|
|
340
|
+
await instance.getRedisClient()?.set(taskKey, 'invalid json {');
|
|
341
|
+
// 应该抛出错误或返回 null
|
|
342
|
+
await expect(instance.testGetTask(taskId)).rejects.toThrow();
|
|
343
|
+
});
|
|
344
|
+
it('应该正确解析包含特殊字符的任务数据', async () => {
|
|
345
|
+
const taskId = 'special-char-task';
|
|
346
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
347
|
+
const specialTask = {
|
|
348
|
+
id: taskId,
|
|
349
|
+
data: {
|
|
350
|
+
message: 'Hello "World" with \n newlines and 中文',
|
|
351
|
+
symbols: '!@#$%^&*()',
|
|
352
|
+
},
|
|
353
|
+
status: 'pending',
|
|
354
|
+
retryCount: 0,
|
|
355
|
+
maxRetries: 3,
|
|
356
|
+
createdTime: new Date().toISOString(),
|
|
357
|
+
};
|
|
358
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(specialTask));
|
|
359
|
+
const retrieved = await instance.testGetTask(taskId);
|
|
360
|
+
expect(retrieved).toEqual(specialTask);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
describe('applyStatus 原子性测试', () => {
|
|
364
|
+
it('应该原子性地更新任务状态和队列位置', async () => {
|
|
365
|
+
const taskId = 'atomic-test-task';
|
|
366
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
367
|
+
const task = {
|
|
368
|
+
id: taskId,
|
|
369
|
+
data: { test: 'data' },
|
|
370
|
+
status: 'pending',
|
|
371
|
+
retryCount: 0,
|
|
372
|
+
maxRetries: 3,
|
|
373
|
+
createdTime: new Date().toISOString(),
|
|
374
|
+
};
|
|
375
|
+
// 初始化任务
|
|
376
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
|
|
377
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:pending`, taskId);
|
|
378
|
+
// 应用状态变更
|
|
379
|
+
await instance.testApplyStatus(taskId, 'pending', 'processing');
|
|
380
|
+
// 验证任务已从 pending 队列移除
|
|
381
|
+
const pendingList = await instance.getRedisClient()?.lRange(`${queueKey}:pending`, 0, -1);
|
|
382
|
+
expect(pendingList).not.toContain(taskId);
|
|
383
|
+
// 验证任务已添加到 processing 队列
|
|
384
|
+
const processingList = await instance.getRedisClient()?.lRange(`${queueKey}:processing`, 0, -1);
|
|
385
|
+
expect(processingList).toContain(taskId);
|
|
386
|
+
// 验证任务状态已更新
|
|
387
|
+
const updatedTask = await instance.testGetTask(taskId);
|
|
388
|
+
expect(updatedTask?.status).toBe('processing');
|
|
389
|
+
});
|
|
390
|
+
it('应该处理状态不变的情况', async () => {
|
|
391
|
+
const taskId = 'same-status-task';
|
|
392
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
393
|
+
const task = {
|
|
394
|
+
id: taskId,
|
|
395
|
+
data: { count: 1 },
|
|
396
|
+
status: 'processing',
|
|
397
|
+
retryCount: 0,
|
|
398
|
+
maxRetries: 3,
|
|
399
|
+
createdTime: new Date().toISOString(),
|
|
400
|
+
};
|
|
401
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
|
|
402
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:processing`, taskId);
|
|
403
|
+
// 应用相同状态的变更
|
|
404
|
+
await instance.testApplyStatus(taskId, 'processing', 'processing');
|
|
405
|
+
// 验证队列中只有一个任务
|
|
406
|
+
const processingList = await instance.getRedisClient()?.lRange(`${queueKey}:processing`, 0, -1);
|
|
407
|
+
expect(processingList?.length).toBe(1);
|
|
408
|
+
});
|
|
409
|
+
it('应该处理任务不存在的情况', async () => {
|
|
410
|
+
// 不应该抛出错误
|
|
411
|
+
await expect(instance.testApplyStatus('non-existent', 'pending', 'processing')).resolves.not.toThrow();
|
|
412
|
+
});
|
|
413
|
+
it('应该支持所有状态之间的转换', async () => {
|
|
414
|
+
const transitions = [
|
|
415
|
+
['pending', 'processing'],
|
|
416
|
+
['processing', 'completed'],
|
|
417
|
+
['processing', 'failed'],
|
|
418
|
+
['failed', 'pending'], // 重试
|
|
419
|
+
];
|
|
420
|
+
for (const [from, to] of transitions) {
|
|
421
|
+
const taskId = `transition-${from}-${to}`;
|
|
422
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
423
|
+
const task = {
|
|
424
|
+
id: taskId,
|
|
425
|
+
data: { transition: `${from}-${to}` },
|
|
426
|
+
status: from,
|
|
427
|
+
retryCount: 0,
|
|
428
|
+
maxRetries: 3,
|
|
429
|
+
createdTime: new Date().toISOString(),
|
|
430
|
+
};
|
|
431
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
|
|
432
|
+
await instance.getRedisClient()?.rPush(instance.testGetQueueByStatus(from), taskId);
|
|
433
|
+
await instance.testApplyStatus(taskId, from, to);
|
|
434
|
+
const updatedTask = await instance.testGetTask(taskId);
|
|
435
|
+
expect(updatedTask?.status).toBe(to);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
describe('statistics 准确性测试', () => {
|
|
440
|
+
it('应该返回空队列的统计信息', async () => {
|
|
441
|
+
const stats = await instance.statistics();
|
|
442
|
+
expect(stats).toEqual({
|
|
443
|
+
pending: 0,
|
|
444
|
+
processing: 0,
|
|
445
|
+
completed: 0,
|
|
446
|
+
failed: 0,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
it('应该准确统计大量任务', async () => {
|
|
450
|
+
const counts = {
|
|
451
|
+
pending: 100,
|
|
452
|
+
processing: 50,
|
|
453
|
+
completed: 200,
|
|
454
|
+
failed: 10,
|
|
455
|
+
};
|
|
456
|
+
// 添加任务到各个队列
|
|
457
|
+
for (const [status, count] of Object.entries(counts)) {
|
|
458
|
+
const queueName = instance.testGetQueueByStatus(status);
|
|
459
|
+
const taskIds = Array.from({ length: count }, (_, i) => `task-${status}-${i}`);
|
|
460
|
+
if (taskIds.length > 0) {
|
|
461
|
+
await instance.getRedisClient()?.rPush(queueName, taskIds);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const stats = await instance.statistics();
|
|
465
|
+
expect(stats).toEqual(counts);
|
|
466
|
+
});
|
|
467
|
+
it('应该处理混合状态的任务', async () => {
|
|
468
|
+
// 添加一些任务到不同队列
|
|
469
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:pending`, ['task1', 'task2', 'task3']);
|
|
470
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:processing`, ['task4']);
|
|
471
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:completed`, ['task5', 'task6']);
|
|
472
|
+
const stats = await instance.statistics();
|
|
473
|
+
expect(stats.pending).toBe(3);
|
|
474
|
+
expect(stats.processing).toBe(1);
|
|
475
|
+
expect(stats.completed).toBe(2);
|
|
476
|
+
expect(stats.failed).toBe(0);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
describe('并发安全测试', () => {
|
|
480
|
+
it('应该支持并发的统计查询', async () => {
|
|
481
|
+
// 添加一些任务
|
|
482
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:pending`, ['task1', 'task2']);
|
|
483
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:processing`, ['task3']);
|
|
484
|
+
// 并发查询统计
|
|
485
|
+
const results = await Promise.all([
|
|
486
|
+
instance.statistics(),
|
|
487
|
+
instance.statistics(),
|
|
488
|
+
instance.statistics(),
|
|
489
|
+
]);
|
|
490
|
+
// 所有结果应该一致
|
|
491
|
+
results.forEach(stats => {
|
|
492
|
+
expect(stats.pending).toBe(2);
|
|
493
|
+
expect(stats.processing).toBe(1);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
it('应该支持并发的健康检查', async () => {
|
|
497
|
+
const results = await Promise.all([
|
|
498
|
+
instance.health(),
|
|
499
|
+
instance.health(),
|
|
500
|
+
instance.health(),
|
|
501
|
+
]);
|
|
502
|
+
results.forEach(result => {
|
|
503
|
+
expect(result).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
it('应该支持并发的任务查询', async () => {
|
|
507
|
+
const taskId = 'concurrent-task';
|
|
508
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
509
|
+
const task = {
|
|
510
|
+
id: taskId,
|
|
511
|
+
data: { concurrent: true },
|
|
512
|
+
status: 'pending',
|
|
513
|
+
retryCount: 0,
|
|
514
|
+
maxRetries: 3,
|
|
515
|
+
createdTime: new Date().toISOString(),
|
|
516
|
+
};
|
|
517
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
|
|
518
|
+
const results = await Promise.all([
|
|
519
|
+
instance.testGetTask(taskId),
|
|
520
|
+
instance.testGetTask(taskId),
|
|
521
|
+
instance.testGetTask(taskId),
|
|
522
|
+
]);
|
|
523
|
+
results.forEach(result => {
|
|
524
|
+
expect(result).toEqual(task);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
describe('内存和性能测试', () => {
|
|
529
|
+
it('应该能处理带有大量数据的任务', async () => {
|
|
530
|
+
const taskId = 'large-data-task';
|
|
531
|
+
const taskKey = `${queueKey}:task:${taskId}`;
|
|
532
|
+
// 创建一个包含大量数据的任务
|
|
533
|
+
const largeData = {
|
|
534
|
+
items: Array.from({ length: 1000 }, (_, i) => ({
|
|
535
|
+
id: i,
|
|
536
|
+
name: `Item ${i}`,
|
|
537
|
+
description: `This is a description for item ${i}`.repeat(10),
|
|
538
|
+
})),
|
|
539
|
+
};
|
|
540
|
+
const task = {
|
|
541
|
+
id: taskId,
|
|
542
|
+
data: largeData,
|
|
543
|
+
status: 'pending',
|
|
544
|
+
retryCount: 0,
|
|
545
|
+
maxRetries: 3,
|
|
546
|
+
createdTime: new Date().toISOString(),
|
|
547
|
+
};
|
|
548
|
+
await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
|
|
549
|
+
const retrieved = await instance.testGetTask(taskId);
|
|
550
|
+
expect(retrieved).toEqual(task);
|
|
551
|
+
expect((retrieved?.data).items.length).toBe(1000);
|
|
552
|
+
});
|
|
553
|
+
it('应该快速执行统计查询', async () => {
|
|
554
|
+
// 添加一些任务
|
|
555
|
+
const taskIds = Array.from({ length: 100 }, (_, i) => `perf-task-${i}`);
|
|
556
|
+
await instance.getRedisClient()?.rPush(`${queueKey}:pending`, taskIds);
|
|
557
|
+
const startTime = Date.now();
|
|
558
|
+
await instance.statistics();
|
|
559
|
+
const duration = Date.now() - startTime;
|
|
560
|
+
// 统计查询应该在100ms内完成
|
|
561
|
+
expect(duration).toBeLessThan(100);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
describe('错误处理', () => {
|
|
565
|
+
// 跳过此测试,因为连接超时等待时间过长
|
|
566
|
+
it.skip('应该优雅处理连接失败', async () => {
|
|
567
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
568
|
+
const badInstance = new TestRedisQueueCommon({
|
|
569
|
+
redisUrl: 'redis://invalid-host:9999',
|
|
570
|
+
queueKey: 'test-bad',
|
|
571
|
+
});
|
|
572
|
+
// 设置超时时间更短,并立即尝试连接
|
|
573
|
+
await expect(badInstance.connect()).rejects.toThrow();
|
|
574
|
+
// 清理
|
|
575
|
+
try {
|
|
576
|
+
badInstance.disconnect();
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// 忽略断开连接的错误
|
|
580
|
+
}
|
|
581
|
+
errorSpy.mockRestore();
|
|
582
|
+
}, 10000); // 增加测试超时时间
|
|
583
|
+
it('应该在断开连接时优雅处理操作', async () => {
|
|
584
|
+
instance.disconnect();
|
|
585
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
586
|
+
// 这些操作不应该抛出错误
|
|
587
|
+
const task = await instance.testGetTask('any-id');
|
|
588
|
+
expect(task).toBeNull();
|
|
589
|
+
const health = await instance.health();
|
|
590
|
+
expect(health).toBe(false);
|
|
591
|
+
const stats = await instance.statistics();
|
|
592
|
+
expect(stats).toEqual({
|
|
593
|
+
pending: 0,
|
|
594
|
+
processing: 0,
|
|
595
|
+
completed: 0,
|
|
596
|
+
failed: 0,
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
describe('队列键名一致性', () => {
|
|
601
|
+
it('所有队列键名应该使用正确的前缀', () => {
|
|
602
|
+
const config = instance.getConfig();
|
|
603
|
+
expect(config.pendingQueue).toContain(queueKey);
|
|
604
|
+
expect(config.processingQueue).toContain(queueKey);
|
|
605
|
+
expect(config.completedQueue).toContain(queueKey);
|
|
606
|
+
expect(config.failedQueue).toContain(queueKey);
|
|
607
|
+
});
|
|
608
|
+
it('应该为不同的 queueKey 生成不同的队列名', () => {
|
|
609
|
+
const instance1 = new TestRedisQueueCommon({
|
|
610
|
+
redisUrl: REDIS_URL,
|
|
611
|
+
queueKey: 'queue-one',
|
|
612
|
+
});
|
|
613
|
+
const instance2 = new TestRedisQueueCommon({
|
|
614
|
+
redisUrl: REDIS_URL,
|
|
615
|
+
queueKey: 'queue-two',
|
|
616
|
+
});
|
|
617
|
+
const config1 = instance1.getConfig();
|
|
618
|
+
const config2 = instance2.getConfig();
|
|
619
|
+
expect(config1.pendingQueue).not.toBe(config2.pendingQueue);
|
|
620
|
+
expect(config1.processingQueue).not.toBe(config2.processingQueue);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
});
|