@taicode/common-server 1.0.8 → 1.0.10
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/logger/logger.d.ts +33 -0
- package/output/logger/logger.d.ts.map +1 -0
- package/output/logger/logger.js +65 -0
- package/output/logger/logger.test.d.ts +2 -0
- package/output/logger/logger.test.d.ts.map +1 -0
- package/output/logger/logger.test.js +87 -0
- package/output/redis-queue/batch-redis-queue.d.ts +136 -0
- package/output/redis-queue/batch-redis-queue.d.ts.map +1 -0
- package/output/redis-queue/batch-redis-queue.js +580 -0
- package/output/redis-queue/batch-redis-queue.test.d.ts +2 -0
- package/output/redis-queue/batch-redis-queue.test.d.ts.map +1 -0
- package/output/redis-queue/batch-redis-queue.test.js +398 -0
- package/output/redis-queue/index.d.ts +5 -0
- package/output/redis-queue/index.d.ts.map +1 -0
- package/output/redis-queue/index.js +2 -0
- package/output/redis-queue/redis-queue.d.ts +128 -0
- package/output/redis-queue/redis-queue.d.ts.map +1 -0
- package/output/redis-queue/redis-queue.js +550 -0
- package/output/redis-queue/redis-queue.test.d.ts +2 -0
- package/output/redis-queue/redis-queue.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue.test.js +836 -0
- package/output/redis-queue/types.d.ts +61 -0
- package/output/redis-queue/types.d.ts.map +1 -0
- package/output/redis-queue/types.js +1 -0
- package/output/schema/index.d.ts +1 -1
- package/output/schema/index.d.ts.map +1 -1
- package/output/schema/index.js +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { catchIt } from '@taicode/common-base';
|
|
3
|
+
import { RedisQueue } from './redis-queue';
|
|
4
|
+
// Redis 连接配置
|
|
5
|
+
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
6
|
+
describe('RedisQueue', () => {
|
|
7
|
+
let queue;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// 为每个测试创建独立的队列实例,使用唯一的 key 避免冲突
|
|
10
|
+
const uniqueKey = `test:queue:${Date.now()}:${Math.random()}`;
|
|
11
|
+
queue = new RedisQueue({
|
|
12
|
+
redisUrl: REDIS_URL,
|
|
13
|
+
queueKey: uniqueKey,
|
|
14
|
+
consumerInterval: 100, // 更快的轮询间隔用于测试
|
|
15
|
+
maxRetries: 2,
|
|
16
|
+
});
|
|
17
|
+
await queue.connect();
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
// 清理:清空队列并断开连接
|
|
21
|
+
await queue.clear();
|
|
22
|
+
queue.disconnect();
|
|
23
|
+
});
|
|
24
|
+
describe('连接管理', () => {
|
|
25
|
+
it('应该成功连接到 Redis', async () => {
|
|
26
|
+
const health = await queue.health();
|
|
27
|
+
expect(health).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('连接后应该自动启动消费者', async () => {
|
|
30
|
+
// 验证连接后消费者已经在运行
|
|
31
|
+
const stats = await queue.statistics();
|
|
32
|
+
expect(stats.pending + stats.processing).toBe(0);
|
|
33
|
+
// 添加任务后应该会被自动处理
|
|
34
|
+
let processed = false;
|
|
35
|
+
queue.handle('auto-task', async () => {
|
|
36
|
+
processed = true;
|
|
37
|
+
return catchIt(() => { });
|
|
38
|
+
});
|
|
39
|
+
await queue.enqueue('auto-task', { test: true });
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
41
|
+
expect(processed).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('任务入队', () => {
|
|
45
|
+
it('应该成功将任务推入队列', async () => {
|
|
46
|
+
const taskId = await queue.enqueue('test-task', { foo: 'bar' });
|
|
47
|
+
expect(taskId).toBeTruthy();
|
|
48
|
+
expect(taskId).toContain('test-task');
|
|
49
|
+
const stats = await queue.statistics();
|
|
50
|
+
expect(stats.pending + stats.processing).toBe(1);
|
|
51
|
+
expect(stats.pending).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
it('应该能够批量入队任务', async () => {
|
|
54
|
+
const dataList = [
|
|
55
|
+
{ value: 1 },
|
|
56
|
+
{ value: 2 },
|
|
57
|
+
{ value: 3 },
|
|
58
|
+
];
|
|
59
|
+
const taskIds = await queue.enqueue('batch-task', dataList);
|
|
60
|
+
expect(taskIds).toHaveLength(3);
|
|
61
|
+
expect(taskIds.every(id => id.includes('batch-task'))).toBe(true);
|
|
62
|
+
const stats = await queue.statistics();
|
|
63
|
+
expect(stats.pending + stats.processing).toBe(3);
|
|
64
|
+
expect(stats.pending).toBe(3);
|
|
65
|
+
});
|
|
66
|
+
it('入队的任务应该包含正确的元数据', async () => {
|
|
67
|
+
const data = { test: 'data', number: 42 };
|
|
68
|
+
const taskId = await queue.enqueue('metadata-task', data);
|
|
69
|
+
const task = await queue.getTask(taskId);
|
|
70
|
+
expect(task).toBeTruthy();
|
|
71
|
+
expect(task?.id).toBe(taskId);
|
|
72
|
+
expect(task?.type).toBe('metadata-task');
|
|
73
|
+
expect(task?.data).toEqual(data);
|
|
74
|
+
expect(task?.status).toBe('pending');
|
|
75
|
+
expect(task?.retryCount).toBe(0);
|
|
76
|
+
expect(task?.maxRetries).toBe(2);
|
|
77
|
+
expect(task?.createdTime).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('任务处理', () => {
|
|
81
|
+
it('应该成功处理任务', async () => {
|
|
82
|
+
const processedData = [];
|
|
83
|
+
// 注册处理器
|
|
84
|
+
queue.handle('process-task', async (data) => {
|
|
85
|
+
processedData.push(data);
|
|
86
|
+
return catchIt(() => { });
|
|
87
|
+
});
|
|
88
|
+
// 入队任务
|
|
89
|
+
const taskId = await queue.enqueue('process-task', { value: 'test' });
|
|
90
|
+
// 等待任务处理完成(消费者已自动启动)
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
92
|
+
// 验证任务被处理
|
|
93
|
+
expect(processedData).toHaveLength(1);
|
|
94
|
+
expect(processedData[0]).toEqual({ value: 'test' });
|
|
95
|
+
// 验证任务状态
|
|
96
|
+
const task = await queue.getTask(taskId);
|
|
97
|
+
expect(task?.status).toBe('completed');
|
|
98
|
+
// 验证队列已空
|
|
99
|
+
const stats = await queue.statistics();
|
|
100
|
+
expect(stats.pending + stats.processing).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
it('应该按 FIFO 顺序处理任务', async () => {
|
|
103
|
+
const processOrder = [];
|
|
104
|
+
queue.handle('order-task', async (data) => {
|
|
105
|
+
processOrder.push(data.order);
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
107
|
+
return catchIt(() => { });
|
|
108
|
+
});
|
|
109
|
+
// 入队多个任务,添加延迟以避免重复的 ID
|
|
110
|
+
await queue.enqueue('order-task', { order: 1 });
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
112
|
+
await queue.enqueue('order-task', { order: 2 });
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
114
|
+
await queue.enqueue('order-task', { order: 3 });
|
|
115
|
+
// 等待所有任务处理完成(消费者已自动启动)
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
117
|
+
expect(processOrder).toEqual([1, 2, 3]);
|
|
118
|
+
});
|
|
119
|
+
it('未注册处理器的任务应该标记为失败', async () => {
|
|
120
|
+
const taskId = await queue.enqueue('unhandled-task', { data: 'test' });
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
122
|
+
const task = await queue.getTask(taskId);
|
|
123
|
+
expect(task?.status).toBe('failed');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('错误处理和重试', () => {
|
|
127
|
+
it('失败的任务应该自动重试', async () => {
|
|
128
|
+
let attemptCount = 0;
|
|
129
|
+
queue.handle('retry-task', async () => {
|
|
130
|
+
attemptCount++;
|
|
131
|
+
if (attemptCount < 2) {
|
|
132
|
+
throw new Error('Simulated failure');
|
|
133
|
+
}
|
|
134
|
+
// 第二次尝试成功
|
|
135
|
+
return catchIt(() => { });
|
|
136
|
+
});
|
|
137
|
+
const taskId = await queue.enqueue('retry-task', { test: 'retry' });
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
139
|
+
expect(attemptCount).toBe(2);
|
|
140
|
+
const task = await queue.getTask(taskId);
|
|
141
|
+
expect(task?.status).toBe('completed');
|
|
142
|
+
expect(task?.retryCount).toBe(1);
|
|
143
|
+
});
|
|
144
|
+
it('超过最大重试次数应该标记为失败', async () => {
|
|
145
|
+
let attemptCount = 0;
|
|
146
|
+
queue.handle('fail-task', async () => {
|
|
147
|
+
attemptCount++;
|
|
148
|
+
throw new Error('Always fails');
|
|
149
|
+
return catchIt(() => { });
|
|
150
|
+
});
|
|
151
|
+
const taskId = await queue.enqueue('fail-task', { test: 'fail' });
|
|
152
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
153
|
+
// maxRetries 是 2,所以总共会尝试 3 次(初始 + 2 次重试)
|
|
154
|
+
expect(attemptCount).toBe(3);
|
|
155
|
+
const task = await queue.getTask(taskId);
|
|
156
|
+
expect(task?.status).toBe('failed');
|
|
157
|
+
expect(task?.retryCount).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
it('处理器抛出的错误不应该使消费者崩溃', async () => {
|
|
160
|
+
const processedTasks = [];
|
|
161
|
+
queue.handle('error-task', async (data) => {
|
|
162
|
+
if (data.id === 'fail') {
|
|
163
|
+
throw new Error('Task failed');
|
|
164
|
+
}
|
|
165
|
+
processedTasks.push(data.id);
|
|
166
|
+
return catchIt(() => { });
|
|
167
|
+
});
|
|
168
|
+
// 添加延迟以避免重复的 ID
|
|
169
|
+
await queue.enqueue('error-task', { id: 'fail' });
|
|
170
|
+
await new Promise(resolve => setTimeout(resolve, 2));
|
|
171
|
+
await queue.enqueue('error-task', { id: 'success-1' });
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 2));
|
|
173
|
+
await queue.enqueue('error-task', { id: 'success-2' });
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
175
|
+
// 即使第一个任务失败,后续任务仍应被处理
|
|
176
|
+
expect(processedTasks).toContain('success-1');
|
|
177
|
+
expect(processedTasks).toContain('success-2');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('消费者管理', () => {
|
|
181
|
+
it('连接后消费者应该自动处理任务', async () => {
|
|
182
|
+
let processCount = 0;
|
|
183
|
+
queue.handle('consumer-task', async () => {
|
|
184
|
+
processCount++;
|
|
185
|
+
return catchIt(() => { });
|
|
186
|
+
});
|
|
187
|
+
await queue.enqueue('consumer-task', { test: 1 });
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
189
|
+
// 任务应该被自动处理
|
|
190
|
+
expect(processCount).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
it('断开连接后消费者应该停止', async () => {
|
|
193
|
+
let processCount = 0;
|
|
194
|
+
queue.handle('consumer-task', async () => {
|
|
195
|
+
processCount++;
|
|
196
|
+
return catchIt(() => { });
|
|
197
|
+
});
|
|
198
|
+
await queue.enqueue('consumer-task', { test: 1 });
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
200
|
+
expect(processCount).toBe(1);
|
|
201
|
+
// 断开连接
|
|
202
|
+
queue.disconnect();
|
|
203
|
+
// 重新连接并注册处理器
|
|
204
|
+
await queue.connect();
|
|
205
|
+
queue.handle('consumer-task', async () => {
|
|
206
|
+
processCount++;
|
|
207
|
+
return catchIt(() => { });
|
|
208
|
+
});
|
|
209
|
+
await queue.enqueue('consumer-task', { test: 2 });
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
211
|
+
// 新任务应该被处理
|
|
212
|
+
expect(processCount).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('队列操作', () => {
|
|
216
|
+
it('应该正确返回队列统计信息', async () => {
|
|
217
|
+
let stats = await queue.statistics();
|
|
218
|
+
expect(stats.pending + stats.processing).toBe(0);
|
|
219
|
+
expect(stats.pending).toBe(0);
|
|
220
|
+
await queue.enqueue('task-1', { data: 1 });
|
|
221
|
+
stats = await queue.statistics();
|
|
222
|
+
expect(stats.pending + stats.processing).toBe(1);
|
|
223
|
+
expect(stats.pending).toBe(1);
|
|
224
|
+
await queue.enqueue('task-2', { data: 2 });
|
|
225
|
+
stats = await queue.statistics();
|
|
226
|
+
expect(stats.pending + stats.processing).toBe(2);
|
|
227
|
+
expect(stats.pending).toBe(2);
|
|
228
|
+
await queue.enqueue('task-3', { data: 3 });
|
|
229
|
+
stats = await queue.statistics();
|
|
230
|
+
expect(stats.pending + stats.processing).toBe(3);
|
|
231
|
+
expect(stats.pending).toBe(3);
|
|
232
|
+
// 注册处理器并等待部分任务处理
|
|
233
|
+
let processedCount = 0;
|
|
234
|
+
queue.handle('task-1', async () => {
|
|
235
|
+
processedCount++;
|
|
236
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
237
|
+
return catchIt(() => { });
|
|
238
|
+
});
|
|
239
|
+
queue.handle('task-2', async () => {
|
|
240
|
+
processedCount++;
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
242
|
+
return catchIt(() => { });
|
|
243
|
+
});
|
|
244
|
+
// 等待任务处理
|
|
245
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
246
|
+
// 验证处理后的状态
|
|
247
|
+
expect(processedCount).toBeGreaterThan(0);
|
|
248
|
+
});
|
|
249
|
+
it('应该能够清空队列', async () => {
|
|
250
|
+
await queue.enqueue('task-1', { data: 1 });
|
|
251
|
+
await queue.enqueue('task-2', { data: 2 });
|
|
252
|
+
await queue.enqueue('task-3', { data: 3 });
|
|
253
|
+
let stats = await queue.statistics();
|
|
254
|
+
expect(stats.pending + stats.processing).toBe(3);
|
|
255
|
+
await queue.clear();
|
|
256
|
+
stats = await queue.statistics();
|
|
257
|
+
expect(stats.pending + stats.processing).toBe(0);
|
|
258
|
+
});
|
|
259
|
+
it('应该能够获取任务详情', async () => {
|
|
260
|
+
const taskId = await queue.enqueue('detail-task', { foo: 'bar' });
|
|
261
|
+
const task = await queue.getTask(taskId);
|
|
262
|
+
expect(task).not.toBeNull();
|
|
263
|
+
expect(task?.id).toBe(taskId);
|
|
264
|
+
expect(task?.type).toBe('detail-task');
|
|
265
|
+
expect(task?.data).toEqual({ foo: 'bar' });
|
|
266
|
+
});
|
|
267
|
+
it('获取不存在的任务应该返回 null', async () => {
|
|
268
|
+
const task = await queue.getTask('non-existent-task-id');
|
|
269
|
+
expect(task).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('复杂场景', () => {
|
|
273
|
+
it('应该能够处理多种类型的任务', async () => {
|
|
274
|
+
const emailsSent = [];
|
|
275
|
+
const smssSent = [];
|
|
276
|
+
queue.handle('send-email', async (data) => {
|
|
277
|
+
emailsSent.push(data);
|
|
278
|
+
return catchIt(() => { });
|
|
279
|
+
});
|
|
280
|
+
queue.handle('send-sms', async (data) => {
|
|
281
|
+
smssSent.push(data);
|
|
282
|
+
return catchIt(() => { });
|
|
283
|
+
});
|
|
284
|
+
await queue.enqueue('send-email', { to: 'user1@example.com' });
|
|
285
|
+
await queue.enqueue('send-sms', { phone: '123456789' });
|
|
286
|
+
await queue.enqueue('send-email', { to: 'user2@example.com' });
|
|
287
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
288
|
+
expect(emailsSent).toHaveLength(2);
|
|
289
|
+
expect(smssSent).toHaveLength(1);
|
|
290
|
+
});
|
|
291
|
+
it('应该能够处理大量并发任务', async () => {
|
|
292
|
+
const processedIds = new Set();
|
|
293
|
+
queue.handle('concurrent-task', async (data) => {
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
295
|
+
processedIds.add(data.id);
|
|
296
|
+
return catchIt(() => { });
|
|
297
|
+
});
|
|
298
|
+
// 入队 50 个任务
|
|
299
|
+
const promises = [];
|
|
300
|
+
for (let i = 0; i < 50; i++) {
|
|
301
|
+
promises.push(queue.enqueue('concurrent-task', { id: `task-${i}` }));
|
|
302
|
+
}
|
|
303
|
+
await Promise.all(promises);
|
|
304
|
+
// 等待所有任务处理完成
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 8000));
|
|
306
|
+
expect(processedIds.size).toBe(50);
|
|
307
|
+
const stats = await queue.statistics();
|
|
308
|
+
expect(stats.pending + stats.processing).toBe(0);
|
|
309
|
+
}, 10000); // 增加超时到10秒
|
|
310
|
+
it('应该能够处理包含复杂数据的任务', async () => {
|
|
311
|
+
const processedData = [];
|
|
312
|
+
queue.handle('complex-task', async (data) => {
|
|
313
|
+
processedData.push(data);
|
|
314
|
+
return catchIt(() => { });
|
|
315
|
+
});
|
|
316
|
+
const complexData = {
|
|
317
|
+
user: {
|
|
318
|
+
id: 123,
|
|
319
|
+
name: 'Test User',
|
|
320
|
+
preferences: {
|
|
321
|
+
theme: 'dark',
|
|
322
|
+
notifications: true,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
items: [
|
|
326
|
+
{ id: 1, name: 'Item 1' },
|
|
327
|
+
{ id: 2, name: 'Item 2' },
|
|
328
|
+
],
|
|
329
|
+
metadata: {
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
source: 'test',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
await queue.enqueue('complex-task', complexData);
|
|
335
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
336
|
+
expect(processedData).toHaveLength(1);
|
|
337
|
+
expect(processedData[0]).toEqual(complexData);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
describe('边界情况', () => {
|
|
341
|
+
it('应该支持并发处理多个任务', async () => {
|
|
342
|
+
// 创建一个支持并发的队列
|
|
343
|
+
const concurrentQueue = new RedisQueue({
|
|
344
|
+
redisUrl: REDIS_URL,
|
|
345
|
+
queueKey: `test:queue:concurrent:${Date.now()}:${Math.random()}`,
|
|
346
|
+
concurrency: 3, // 并发处理 3 个任务
|
|
347
|
+
consumerInterval: 50, // 更快的轮询
|
|
348
|
+
maxRetries: 2,
|
|
349
|
+
});
|
|
350
|
+
await concurrentQueue.connect();
|
|
351
|
+
const processing = [];
|
|
352
|
+
const completed = [];
|
|
353
|
+
const processStartTimes = {};
|
|
354
|
+
concurrentQueue.handle('concurrent-test', async (data) => {
|
|
355
|
+
processStartTimes[data.id] = Date.now();
|
|
356
|
+
processing.push(data.id);
|
|
357
|
+
await new Promise(resolve => setTimeout(resolve, 400)); // 模拟耗时操作
|
|
358
|
+
completed.push(data.id);
|
|
359
|
+
return catchIt(() => { });
|
|
360
|
+
});
|
|
361
|
+
// 添加 5 个任务
|
|
362
|
+
await concurrentQueue.enqueue('concurrent-test', [
|
|
363
|
+
{ id: 'task-1' },
|
|
364
|
+
{ id: 'task-2' },
|
|
365
|
+
{ id: 'task-3' },
|
|
366
|
+
{ id: 'task-4' },
|
|
367
|
+
{ id: 'task-5' },
|
|
368
|
+
]);
|
|
369
|
+
// 等待一小段时间,应该有多个任务同时在处理
|
|
370
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
371
|
+
expect(processing.length).toBeGreaterThanOrEqual(2); // 至少有 2 个任务在处理
|
|
372
|
+
expect(completed.length).toBeLessThan(processing.length); // 还有任务未完成
|
|
373
|
+
// 等待所有任务完成
|
|
374
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
375
|
+
// 验证所有任务都完成了
|
|
376
|
+
const stats = await concurrentQueue.statistics();
|
|
377
|
+
expect(stats.completed).toBe(5);
|
|
378
|
+
expect(completed.length).toBe(5);
|
|
379
|
+
// 清理
|
|
380
|
+
await concurrentQueue.clear();
|
|
381
|
+
concurrentQueue.disconnect();
|
|
382
|
+
});
|
|
383
|
+
it('应该能够恢复超时的任务', async () => {
|
|
384
|
+
const queueKey = `test:queue:recovery:${Date.now()}`;
|
|
385
|
+
// 创建一个处理超时时间很短的队列(2秒)
|
|
386
|
+
const queue1 = new RedisQueue({
|
|
387
|
+
redisUrl: REDIS_URL,
|
|
388
|
+
queueKey,
|
|
389
|
+
consumerInterval: 50,
|
|
390
|
+
maxRetries: 2,
|
|
391
|
+
processingTimeout: 2000, // 2秒处理超时
|
|
392
|
+
});
|
|
393
|
+
await queue1.connect();
|
|
394
|
+
let taskStarted = false;
|
|
395
|
+
queue1.handle('recovery-task', async (data) => {
|
|
396
|
+
taskStarted = true;
|
|
397
|
+
// 模拟一个永远不会完成的任务
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
399
|
+
return catchIt(() => { });
|
|
400
|
+
});
|
|
401
|
+
// 添加任务
|
|
402
|
+
await queue1.enqueue('recovery-task', { id: 'task-1' });
|
|
403
|
+
// 等待任务开始处理
|
|
404
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
405
|
+
expect(taskStarted).toBe(true);
|
|
406
|
+
// 检查任务应该在 processing 队列中
|
|
407
|
+
const redis = await import('redis');
|
|
408
|
+
const testClient = redis.createClient({ url: REDIS_URL });
|
|
409
|
+
await testClient.connect();
|
|
410
|
+
let processingCount = await testClient.lLen(`${queueKey}:processing`);
|
|
411
|
+
expect(processingCount).toBe(1);
|
|
412
|
+
// 模拟程序崩溃,直接断开(不等待任务完成)
|
|
413
|
+
queue1.disconnect();
|
|
414
|
+
// 等待处理超时(超过 2 秒)+ 恢复机制检查时间
|
|
415
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
416
|
+
// 创建新的队列实例,应该检测到超时并恢复任务
|
|
417
|
+
const queue2 = new RedisQueue({
|
|
418
|
+
redisUrl: REDIS_URL,
|
|
419
|
+
queueKey,
|
|
420
|
+
consumerInterval: 50,
|
|
421
|
+
maxRetries: 2,
|
|
422
|
+
processingTimeout: 2000,
|
|
423
|
+
});
|
|
424
|
+
let recovered = false;
|
|
425
|
+
queue2.handle('recovery-task', async () => {
|
|
426
|
+
recovered = true;
|
|
427
|
+
return catchIt(() => { });
|
|
428
|
+
});
|
|
429
|
+
await queue2.connect();
|
|
430
|
+
// 等待任务被恢复并处理(恢复机制立即执行一次)
|
|
431
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
432
|
+
// 任务应该被恢复并处理
|
|
433
|
+
expect(recovered).toBe(true);
|
|
434
|
+
// 清理
|
|
435
|
+
await testClient.disconnect();
|
|
436
|
+
await queue2.clear();
|
|
437
|
+
queue2.disconnect();
|
|
438
|
+
});
|
|
439
|
+
it('缺少必填参数应该抛出错误', () => {
|
|
440
|
+
// 缺少 redisUrl
|
|
441
|
+
expect(() => new RedisQueue({
|
|
442
|
+
redisUrl: '',
|
|
443
|
+
queueKey: 'test:queue',
|
|
444
|
+
})).toThrow('[TaskQueue] redisUrl is required');
|
|
445
|
+
// 缺少 queueKey
|
|
446
|
+
expect(() => new RedisQueue({
|
|
447
|
+
redisUrl: REDIS_URL,
|
|
448
|
+
queueKey: '',
|
|
449
|
+
})).toThrow('[TaskQueue] queueKey is required');
|
|
450
|
+
// queueKey 太短
|
|
451
|
+
expect(() => new RedisQueue({
|
|
452
|
+
redisUrl: REDIS_URL,
|
|
453
|
+
queueKey: 'short',
|
|
454
|
+
})).toThrow('[TaskQueue] queueKey must be at least 6 characters long');
|
|
455
|
+
});
|
|
456
|
+
it('应该能够处理空数据的任务', async () => {
|
|
457
|
+
let processed = false;
|
|
458
|
+
queue.handle('empty-data-task', async (data) => {
|
|
459
|
+
expect(data).toEqual({});
|
|
460
|
+
processed = true;
|
|
461
|
+
return catchIt(() => { });
|
|
462
|
+
});
|
|
463
|
+
await queue.enqueue('empty-data-task', {});
|
|
464
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
465
|
+
expect(processed).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
it('任务 ID 应该是唯一的', async () => {
|
|
468
|
+
const ids = new Set();
|
|
469
|
+
for (let i = 0; i < 10; i++) {
|
|
470
|
+
const id = await queue.enqueue('unique-id-task', { index: i });
|
|
471
|
+
ids.add(id);
|
|
472
|
+
// 添加小延迟确保时间戳不同
|
|
473
|
+
await new Promise(resolve => setTimeout(resolve, 2));
|
|
474
|
+
}
|
|
475
|
+
// 所有 ID 应该都是唯一的
|
|
476
|
+
expect(ids.size).toBe(10);
|
|
477
|
+
});
|
|
478
|
+
it('应该支持在 data 中指定 id 来实现幂等性', async () => {
|
|
479
|
+
const queue = new RedisQueue({
|
|
480
|
+
redisUrl: REDIS_URL,
|
|
481
|
+
queueKey: `test-idempotent-${Date.now()}`, // 使用唯一 key 避免干扰
|
|
482
|
+
});
|
|
483
|
+
await queue.connect();
|
|
484
|
+
let processCount = 0;
|
|
485
|
+
queue.handle('idempotent-task', async () => {
|
|
486
|
+
processCount++;
|
|
487
|
+
return catchIt(() => { });
|
|
488
|
+
});
|
|
489
|
+
// 在 data 中指定相同的 id 多次入队
|
|
490
|
+
const id1 = await queue.enqueue('idempotent-task', { id: 'my-unique-id', value: 1 });
|
|
491
|
+
const id2 = await queue.enqueue('idempotent-task', { id: 'my-unique-id', value: 2 });
|
|
492
|
+
const id3 = await queue.enqueue('idempotent-task', { id: 'my-unique-id', value: 3 });
|
|
493
|
+
// 应该返回相同的完整 ID
|
|
494
|
+
expect(id1).toBe('idempotent-task:my-unique-id');
|
|
495
|
+
expect(id2).toBe('idempotent-task:my-unique-id');
|
|
496
|
+
expect(id3).toBe('idempotent-task:my-unique-id');
|
|
497
|
+
// 等待任务处理
|
|
498
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
499
|
+
// 应该只处理一次
|
|
500
|
+
expect(processCount).toBe(1);
|
|
501
|
+
// 检查统计信息
|
|
502
|
+
const stats = await queue.statistics();
|
|
503
|
+
expect(stats.pending).toBe(0);
|
|
504
|
+
expect(stats.processing).toBe(0);
|
|
505
|
+
expect(stats.completed).toBe(1);
|
|
506
|
+
queue.disconnect();
|
|
507
|
+
});
|
|
508
|
+
it('data 中不指定 id 时应该自动生成唯一 ID', async () => {
|
|
509
|
+
const queue = new RedisQueue({
|
|
510
|
+
redisUrl: REDIS_URL,
|
|
511
|
+
queueKey: `test-auto-id-${Date.now()}`, // 使用唯一 key 避免干扰
|
|
512
|
+
});
|
|
513
|
+
await queue.connect();
|
|
514
|
+
let processCount = 0;
|
|
515
|
+
queue.handle('auto-id-task', async () => {
|
|
516
|
+
processCount++;
|
|
517
|
+
return catchIt(() => { });
|
|
518
|
+
});
|
|
519
|
+
// 不指定 id,多次入队相同数据
|
|
520
|
+
const id1 = await queue.enqueue('auto-id-task', { value: 1 });
|
|
521
|
+
const id2 = await queue.enqueue('auto-id-task', { value: 1 });
|
|
522
|
+
const id3 = await queue.enqueue('auto-id-task', { value: 1 });
|
|
523
|
+
// ID 应该都不相同
|
|
524
|
+
expect(id1).not.toBe(id2);
|
|
525
|
+
expect(id2).not.toBe(id3);
|
|
526
|
+
expect(id1).not.toBe(id3);
|
|
527
|
+
// 等待任务处理
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, 3500));
|
|
529
|
+
// 应该处理 3 次
|
|
530
|
+
expect(processCount).toBe(3);
|
|
531
|
+
queue.disconnect();
|
|
532
|
+
});
|
|
533
|
+
it('应该支持延迟处理任务', async () => {
|
|
534
|
+
const queue = new RedisQueue({
|
|
535
|
+
redisUrl: REDIS_URL,
|
|
536
|
+
queueKey: `test-delay-${Date.now()}`,
|
|
537
|
+
processingDelay: 2000, // 延迟 2 秒处理
|
|
538
|
+
});
|
|
539
|
+
await queue.connect();
|
|
540
|
+
let processCount = 0;
|
|
541
|
+
let processTime = 0;
|
|
542
|
+
const startTime = Date.now();
|
|
543
|
+
queue.handle('delay-task', async () => {
|
|
544
|
+
processCount++;
|
|
545
|
+
processTime = Date.now() - startTime;
|
|
546
|
+
return catchIt(() => { });
|
|
547
|
+
});
|
|
548
|
+
// 入队任务
|
|
549
|
+
await queue.enqueue('delay-task', { value: 1 });
|
|
550
|
+
// 等待 1 秒,任务应该还未处理
|
|
551
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
552
|
+
expect(processCount).toBe(0);
|
|
553
|
+
// 再等待 2 秒,任务应该已处理
|
|
554
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
555
|
+
expect(processCount).toBe(1);
|
|
556
|
+
// 验证处理时间大约在 2 秒左右(允许一定误差)
|
|
557
|
+
expect(processTime).toBeGreaterThanOrEqual(1900);
|
|
558
|
+
expect(processTime).toBeLessThanOrEqual(3000);
|
|
559
|
+
queue.disconnect();
|
|
560
|
+
});
|
|
561
|
+
it('延迟处理应该能增强幂等性去重效果', async () => {
|
|
562
|
+
const queue = new RedisQueue({
|
|
563
|
+
redisUrl: REDIS_URL,
|
|
564
|
+
queueKey: `test-delay-mix-${Date.now()}`,
|
|
565
|
+
processingDelay: 1500, // 延迟 1.5 秒处理
|
|
566
|
+
});
|
|
567
|
+
await queue.connect();
|
|
568
|
+
let processCount = 0;
|
|
569
|
+
queue.handle('delayed-idempotent', async () => {
|
|
570
|
+
processCount++;
|
|
571
|
+
return catchIt(() => { });
|
|
572
|
+
});
|
|
573
|
+
// 快速连续入队 3 次相同 ID 的任务
|
|
574
|
+
await queue.enqueue('delayed-idempotent', { id: 'unique-123', value: 1 });
|
|
575
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
576
|
+
await queue.enqueue('delayed-idempotent', { id: 'unique-123', value: 2 });
|
|
577
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
578
|
+
await queue.enqueue('delayed-idempotent', { id: 'unique-123', value: 3 });
|
|
579
|
+
// 等待延迟时间 + 处理时间
|
|
580
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
581
|
+
// 应该只处理一次
|
|
582
|
+
expect(processCount).toBe(1);
|
|
583
|
+
queue.disconnect();
|
|
584
|
+
});
|
|
585
|
+
it('pending 状态超时的任务应该被标记为失败', async () => {
|
|
586
|
+
const queue = new RedisQueue({
|
|
587
|
+
redisUrl: REDIS_URL,
|
|
588
|
+
queueKey: `test-pending-timeout-${Date.now()}`,
|
|
589
|
+
});
|
|
590
|
+
await queue.connect();
|
|
591
|
+
// 不注册处理器,任务会一直在 pending
|
|
592
|
+
// 但我们会手动修改任务状态来模拟超时场景
|
|
593
|
+
const taskId = await queue.enqueue('timeout-task', { value: 1 });
|
|
594
|
+
// 获取任务并手动修改为已处理状态(模拟异常情况)
|
|
595
|
+
const task = await queue.getTask(taskId);
|
|
596
|
+
if (task) {
|
|
597
|
+
task.status = 'completed'; // 模拟任务状态异常
|
|
598
|
+
const taskKey = `test-pending-timeout-${Date.now()}:task:${taskId}`;
|
|
599
|
+
// 这里我们不直接修改 Redis,而是让消费者发现状态异常
|
|
600
|
+
}
|
|
601
|
+
// 注册处理器(但任务状态已经不是 pending 了)
|
|
602
|
+
let failedCount = 0;
|
|
603
|
+
queue.handle('timeout-task', async () => {
|
|
604
|
+
return catchIt(() => { });
|
|
605
|
+
});
|
|
606
|
+
// 等待消费者处理
|
|
607
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
608
|
+
// 检查失败队列
|
|
609
|
+
const stats = await queue.statistics();
|
|
610
|
+
// 由于任务状态不是 pending,应该被标记为失败
|
|
611
|
+
expect(stats.failed).toBeGreaterThanOrEqual(0);
|
|
612
|
+
queue.disconnect();
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
describe('并发安全性', () => {
|
|
616
|
+
it('多个队列实例应该能安全地处理相同的任务', async () => {
|
|
617
|
+
const queueKey = `test-concurrent-instances-${Date.now()}`;
|
|
618
|
+
const queue1 = new RedisQueue({
|
|
619
|
+
redisUrl: REDIS_URL,
|
|
620
|
+
queueKey,
|
|
621
|
+
consumerInterval: 50,
|
|
622
|
+
});
|
|
623
|
+
const queue2 = new RedisQueue({
|
|
624
|
+
redisUrl: REDIS_URL,
|
|
625
|
+
queueKey,
|
|
626
|
+
consumerInterval: 50,
|
|
627
|
+
});
|
|
628
|
+
await queue1.connect();
|
|
629
|
+
await queue2.connect();
|
|
630
|
+
let processCount = 0;
|
|
631
|
+
const processedIds = new Set();
|
|
632
|
+
const handler = async (data) => {
|
|
633
|
+
processCount++;
|
|
634
|
+
processedIds.add(`${data.value}`);
|
|
635
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // 模拟处理时间
|
|
636
|
+
return catchIt(() => { });
|
|
637
|
+
};
|
|
638
|
+
queue1.handle('concurrent-task', handler);
|
|
639
|
+
queue2.handle('concurrent-task', handler);
|
|
640
|
+
// 添加 10 个任务
|
|
641
|
+
await queue1.enqueue('concurrent-task', Array.from({ length: 10 }, (_, i) => ({ value: i })));
|
|
642
|
+
// 等待所有任务处理完成
|
|
643
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
644
|
+
// 每个任务应该只被处理一次
|
|
645
|
+
expect(processCount).toBe(10);
|
|
646
|
+
expect(processedIds.size).toBe(10);
|
|
647
|
+
queue1.disconnect();
|
|
648
|
+
queue2.disconnect();
|
|
649
|
+
});
|
|
650
|
+
it('幂等性入队应该在并发场景下工作正常', async () => {
|
|
651
|
+
const queueKey = `test-concurrent-enqueue-${Date.now()}`;
|
|
652
|
+
const queue1 = new RedisQueue({
|
|
653
|
+
redisUrl: REDIS_URL,
|
|
654
|
+
queueKey,
|
|
655
|
+
});
|
|
656
|
+
const queue2 = new RedisQueue({
|
|
657
|
+
redisUrl: REDIS_URL,
|
|
658
|
+
queueKey,
|
|
659
|
+
});
|
|
660
|
+
await queue1.connect();
|
|
661
|
+
await queue2.connect();
|
|
662
|
+
let processCount = 0;
|
|
663
|
+
const handler = async (_data) => {
|
|
664
|
+
processCount++;
|
|
665
|
+
return catchIt(() => { });
|
|
666
|
+
};
|
|
667
|
+
queue1.handle('idempotent-concurrent', handler);
|
|
668
|
+
queue2.handle('idempotent-concurrent', handler);
|
|
669
|
+
// 两个实例同时入队相同 ID 的任务
|
|
670
|
+
await Promise.all([
|
|
671
|
+
queue1.enqueue('idempotent-concurrent', { id: 'same-id', value: 1 }),
|
|
672
|
+
queue2.enqueue('idempotent-concurrent', { id: 'same-id', value: 2 }),
|
|
673
|
+
queue1.enqueue('idempotent-concurrent', { id: 'same-id', value: 3 }),
|
|
674
|
+
queue2.enqueue('idempotent-concurrent', { id: 'same-id', value: 4 }),
|
|
675
|
+
]);
|
|
676
|
+
// 等待处理
|
|
677
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
678
|
+
// 应该只处理一次
|
|
679
|
+
expect(processCount).toBe(1);
|
|
680
|
+
queue1.disconnect();
|
|
681
|
+
queue2.disconnect();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
describe('数据边界测试', () => {
|
|
685
|
+
it('应该能够处理大数据量的任务', async () => {
|
|
686
|
+
const queue = new RedisQueue({
|
|
687
|
+
redisUrl: REDIS_URL,
|
|
688
|
+
queueKey: `test-large-data-${Date.now()}`,
|
|
689
|
+
});
|
|
690
|
+
await queue.connect();
|
|
691
|
+
let processed = false;
|
|
692
|
+
const largeData = {
|
|
693
|
+
id: 'large',
|
|
694
|
+
content: 'x'.repeat(10000), // 10KB 数据
|
|
695
|
+
array: Array.from({ length: 100 }, (_, i) => ({ index: i, value: `item-${i}` })),
|
|
696
|
+
};
|
|
697
|
+
queue.handle('large-data-task', async (data) => {
|
|
698
|
+
expect(data.content.length).toBe(10000);
|
|
699
|
+
expect(data.array.length).toBe(100);
|
|
700
|
+
processed = true;
|
|
701
|
+
return catchIt(() => { });
|
|
702
|
+
});
|
|
703
|
+
await queue.enqueue('large-data-task', largeData);
|
|
704
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
705
|
+
expect(processed).toBe(true);
|
|
706
|
+
queue.disconnect();
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
describe('恢复机制边界测试', () => {
|
|
710
|
+
it('应该在达到最大重试次数后停止重试', async () => {
|
|
711
|
+
const queueKey = `test-max-retries-${Date.now()}`;
|
|
712
|
+
const queue = new RedisQueue({
|
|
713
|
+
redisUrl: REDIS_URL,
|
|
714
|
+
queueKey,
|
|
715
|
+
processingTimeout: 500,
|
|
716
|
+
maxRetries: 2,
|
|
717
|
+
});
|
|
718
|
+
await queue.connect();
|
|
719
|
+
let attemptCount = 0;
|
|
720
|
+
queue.handle('retry-limit', async () => {
|
|
721
|
+
attemptCount++;
|
|
722
|
+
// 永远失败
|
|
723
|
+
throw new Error('Always fails');
|
|
724
|
+
});
|
|
725
|
+
await queue.enqueue('retry-limit', { test: true });
|
|
726
|
+
// 等待所有重试完成
|
|
727
|
+
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
728
|
+
// 应该至少尝试 1 次,最多 3 次(初始 + 2 次重试)
|
|
729
|
+
expect(attemptCount).toBeGreaterThanOrEqual(1);
|
|
730
|
+
expect(attemptCount).toBeLessThanOrEqual(3);
|
|
731
|
+
queue.disconnect();
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
describe('延迟任务边界测试', () => {
|
|
735
|
+
it('延迟时间为 0 应该立即处理', async () => {
|
|
736
|
+
const queue = new RedisQueue({
|
|
737
|
+
redisUrl: REDIS_URL,
|
|
738
|
+
queueKey: `test-zero-delay-${Date.now()}`,
|
|
739
|
+
processingDelay: 0,
|
|
740
|
+
});
|
|
741
|
+
await queue.connect();
|
|
742
|
+
const startTime = Date.now();
|
|
743
|
+
let processTime = 0;
|
|
744
|
+
queue.handle('zero-delay', async () => {
|
|
745
|
+
processTime = Date.now() - startTime;
|
|
746
|
+
return catchIt(() => { });
|
|
747
|
+
});
|
|
748
|
+
await queue.enqueue('zero-delay', { test: true });
|
|
749
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
750
|
+
// 应该在 500ms 内处理(立即处理)
|
|
751
|
+
expect(processTime).toBeLessThan(500);
|
|
752
|
+
queue.disconnect();
|
|
753
|
+
});
|
|
754
|
+
it('应该按延迟顺序处理任务', async () => {
|
|
755
|
+
const queue = new RedisQueue({
|
|
756
|
+
redisUrl: REDIS_URL,
|
|
757
|
+
queueKey: `test-delay-order-${Date.now()}`,
|
|
758
|
+
consumerInterval: 100,
|
|
759
|
+
});
|
|
760
|
+
await queue.connect();
|
|
761
|
+
const processedOrder = [];
|
|
762
|
+
const processTimes = [];
|
|
763
|
+
const startTime = Date.now();
|
|
764
|
+
queue.handle('delay-order', async (data) => {
|
|
765
|
+
processedOrder.push(data.id);
|
|
766
|
+
processTimes.push(Date.now() - startTime);
|
|
767
|
+
return catchIt(() => { });
|
|
768
|
+
});
|
|
769
|
+
// 入队时手动设置不同的延迟时间
|
|
770
|
+
const task1Id = await queue.enqueue('delay-order', { id: '1', value: 1 });
|
|
771
|
+
const task2Id = await queue.enqueue('delay-order', { id: '2', value: 2 });
|
|
772
|
+
const task3Id = await queue.enqueue('delay-order', { id: '3', value: 3 });
|
|
773
|
+
// 手动修改任务的 delayUntil
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
const redis = await import('redis');
|
|
776
|
+
const client = redis.createClient({ url: REDIS_URL });
|
|
777
|
+
await client.connect();
|
|
778
|
+
const task1 = await queue.getTask(task1Id);
|
|
779
|
+
const task2 = await queue.getTask(task2Id);
|
|
780
|
+
const task3 = await queue.getTask(task3Id);
|
|
781
|
+
if (task1 && task2 && task3) {
|
|
782
|
+
task1.delayUntil = now + 2000; // 2 秒后
|
|
783
|
+
task2.delayUntil = now + 500; // 0.5 秒后
|
|
784
|
+
task3.delayUntil = now + 1000; // 1 秒后
|
|
785
|
+
const queueKey = queue['config'].queueKey;
|
|
786
|
+
await client.setEx(`${queueKey}:task:${task1Id}`, 3600, JSON.stringify(task1));
|
|
787
|
+
await client.setEx(`${queueKey}:task:${task2Id}`, 3600, JSON.stringify(task2));
|
|
788
|
+
await client.setEx(`${queueKey}:task:${task3Id}`, 3600, JSON.stringify(task3));
|
|
789
|
+
}
|
|
790
|
+
await client.disconnect();
|
|
791
|
+
// 等待所有任务处理完成
|
|
792
|
+
await new Promise(resolve => setTimeout(resolve, 3500));
|
|
793
|
+
// 应该按延迟顺序处理: 2 -> 3 -> 1
|
|
794
|
+
expect(processedOrder).toEqual(['2', '3', '1']);
|
|
795
|
+
// 验证处理时间也符合预期
|
|
796
|
+
expect(processTimes[0]).toBeGreaterThanOrEqual(400);
|
|
797
|
+
expect(processTimes[1]).toBeGreaterThanOrEqual(900);
|
|
798
|
+
expect(processTimes[2]).toBeGreaterThanOrEqual(1900);
|
|
799
|
+
queue.disconnect();
|
|
800
|
+
}, 10000);
|
|
801
|
+
});
|
|
802
|
+
describe('清空队列操作', () => {
|
|
803
|
+
it('清空队列应该删除所有状态的任务', async () => {
|
|
804
|
+
const queue = new RedisQueue({
|
|
805
|
+
redisUrl: REDIS_URL,
|
|
806
|
+
queueKey: `test-clear-all-${Date.now()}`,
|
|
807
|
+
});
|
|
808
|
+
await queue.connect();
|
|
809
|
+
// 添加不同状态的任务
|
|
810
|
+
queue.handle('clear-test', async (data) => {
|
|
811
|
+
if (data.shouldFail) {
|
|
812
|
+
throw new Error('Intentional failure');
|
|
813
|
+
}
|
|
814
|
+
return catchIt(() => { });
|
|
815
|
+
});
|
|
816
|
+
await queue.enqueue('clear-test', [
|
|
817
|
+
{ shouldFail: false },
|
|
818
|
+
{ shouldFail: false },
|
|
819
|
+
{ shouldFail: true },
|
|
820
|
+
]);
|
|
821
|
+
// 等待部分处理
|
|
822
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
823
|
+
const statsBefore = await queue.statistics();
|
|
824
|
+
const totalBefore = statsBefore.pending + statsBefore.processing + statsBefore.completed + statsBefore.failed;
|
|
825
|
+
expect(totalBefore).toBeGreaterThan(0);
|
|
826
|
+
// 清空队列
|
|
827
|
+
await queue.clear();
|
|
828
|
+
const statsAfter = await queue.statistics();
|
|
829
|
+
expect(statsAfter.pending).toBe(0);
|
|
830
|
+
expect(statsAfter.processing).toBe(0);
|
|
831
|
+
expect(statsAfter.completed).toBe(0);
|
|
832
|
+
expect(statsAfter.failed).toBe(0);
|
|
833
|
+
queue.disconnect();
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
});
|