queen-mq 0.3.1 → 0.4.0

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.
Files changed (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +1 -1
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -1,1137 +0,0 @@
1
- /**
2
- * Advanced Pattern Tests for Queen Message Queue
3
- * Tests: Multi-stage pipeline, fan-out/fan-in, DLQ, circuit breaker, and more
4
- */
5
-
6
- import crypto from 'crypto';
7
- import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log } from './utils.js';
8
-
9
- /**
10
- * Test: Multi-stage pipeline workflow
11
- */
12
- export async function testMultiStagePipeline(client) {
13
- startTest('Multi-Stage Pipeline Workflow', 'workflow');
14
-
15
- try {
16
- const stages = ['ingestion', 'validation', 'processing', 'enrichment', 'delivery'];
17
- const itemCount = 10;
18
-
19
- // Configure queues for each stage
20
- for (const stage of stages) {
21
- await client.queue(`workflow-${stage}`, {
22
- priority: stages.indexOf(stage) + 1
23
- });
24
- }
25
-
26
- // Push initial items to ingestion
27
- const startTime = Date.now();
28
- const items = Array.from({ length: itemCount }, (_, i) => ({
29
- id: i,
30
- data: `Item ${i}`,
31
- stage: 'ingestion',
32
- timestamp: Date.now()
33
- }));
34
-
35
- await client.push('workflow-ingestion', items);
36
- await sleep(200); // Wait for messages to be available
37
-
38
- // Process through each stage
39
- for (let stageIndex = 0; stageIndex < stages.length - 1; stageIndex++) {
40
- const currentStage = stages[stageIndex];
41
- const nextStage = stages[stageIndex + 1];
42
-
43
- // Process current stage with larger batch size
44
- const stageMessages = [];
45
- for await (const msg of client.take(`workflow-${currentStage}`, {
46
- limit: itemCount,
47
- batch: itemCount
48
- })) {
49
- stageMessages.push(msg);
50
- // ACK immediately to release partition lock for next takes
51
- await client.ack(msg);
52
- }
53
-
54
- if (stageMessages.length > 0) {
55
- // Process and move to next stage
56
- const nextItems = stageMessages.map(msg => ({
57
- ...msg.data,
58
- stage: nextStage,
59
- previousStage: currentStage,
60
- processingTime: Date.now() - msg.data.timestamp
61
- }));
62
-
63
- await client.push(`workflow-${nextStage}`, nextItems);
64
- }
65
- }
66
-
67
- // Verify final delivery
68
- const finalMessages = [];
69
- for await (const msg of client.take('workflow-delivery', { limit: itemCount })) {
70
- finalMessages.push(msg);
71
- await client.ack(msg);
72
- }
73
-
74
- if (finalMessages.length !== itemCount) {
75
- throw new Error(`Pipeline incomplete: expected ${itemCount} items in delivery`);
76
- }
77
-
78
- const totalTime = Date.now() - startTime;
79
- const avgTime = Math.round(totalTime / itemCount);
80
-
81
- passTest(`Pipeline processed ${itemCount} items through ${stages.length} stages (avg ${avgTime}ms)`);
82
- } catch (error) {
83
- failTest(error);
84
- }
85
- }
86
-
87
- /**
88
- * Test: Fan-out/Fan-in pattern
89
- */
90
- export async function testFanOutFanIn(client) {
91
- startTest('Fan-out/Fan-in Pattern', 'pattern');
92
-
93
- try {
94
- const masterQueue = 'pattern-master';
95
- const workerQueues = ['pattern-worker-1', 'pattern-worker-2', 'pattern-worker-3'];
96
- const aggregatorQueue = 'pattern-aggregator';
97
-
98
- // Configure queues
99
- await client.queue(masterQueue, {});
100
- for (const worker of workerQueues) {
101
- await client.queue(worker, {});
102
- }
103
- await client.queue(aggregatorQueue, {});
104
-
105
- // Push master task
106
- const masterTask = {
107
- id: crypto.randomBytes(16).toString('hex'),
108
- totalWork: 9,
109
- data: Array.from({ length: 9 }, (_, i) => `Task ${i}`)
110
- };
111
-
112
- await client.push(masterQueue, masterTask);
113
-
114
- // Take master task
115
- let masterTaskData = null;
116
- for await (const msg of client.take(masterQueue, { limit: 1 })) {
117
- masterTaskData = msg.data;
118
- await client.ack(msg);
119
- }
120
-
121
- if (!masterTaskData) {
122
- throw new Error('Failed to get master task');
123
- }
124
-
125
- // Fan-out to workers
126
- const tasksPerWorker = Math.ceil(masterTaskData.totalWork / workerQueues.length);
127
- for (let i = 0; i < workerQueues.length; i++) {
128
- const startIdx = i * tasksPerWorker;
129
- const endIdx = Math.min(startIdx + tasksPerWorker, masterTaskData.totalWork);
130
- const workerTasks = masterTaskData.data.slice(startIdx, endIdx);
131
-
132
- await client.push(workerQueues[i], {
133
- masterId: masterTaskData.id,
134
- workerIndex: i,
135
- tasks: workerTasks
136
- });
137
- }
138
-
139
- // Workers process and fan-in to aggregator
140
- const workerResults = [];
141
- for (const workerQueue of workerQueues) {
142
- for await (const msg of client.take(workerQueue, { limit: 1 })) {
143
- // Simulate processing
144
- const processed = {
145
- masterId: msg.data.masterId,
146
- workerIndex: msg.data.workerIndex,
147
- results: msg.data.tasks.map(t => `Processed: ${t}`)
148
- };
149
-
150
- workerResults.push(processed);
151
-
152
- // Send to aggregator
153
- await client.push(aggregatorQueue, processed);
154
- await client.ack(msg);
155
- }
156
- }
157
-
158
- // Aggregate results
159
- const aggregatedResults = [];
160
- for await (const msg of client.take(aggregatorQueue, { limit: workerQueues.length })) {
161
- aggregatedResults.push(msg.data);
162
- await client.ack(msg);
163
- }
164
-
165
- // Verify all tasks were processed
166
- const totalProcessed = aggregatedResults.reduce((sum, r) => sum + r.results.length, 0);
167
- if (totalProcessed !== masterTask.totalWork) {
168
- throw new Error(`Expected ${masterTask.totalWork} tasks processed, got ${totalProcessed}`);
169
- }
170
-
171
- passTest(`Fan-out/Fan-in: ${masterTask.totalWork} tasks distributed to ${workerQueues.length} workers and aggregated`);
172
- } catch (error) {
173
- failTest(error);
174
- }
175
- }
176
-
177
- /**
178
- * Test: Dead letter queue pattern
179
- */
180
- export async function testDeadLetterQueuePattern(client) {
181
- startTest('Dead Letter Queue Pattern', 'pattern');
182
-
183
- try {
184
- const mainQueue = 'pattern-main-dlq';
185
- const dlq = 'pattern-dead-letter';
186
-
187
- // Configure main queue with low retry limit
188
- await client.queue(mainQueue, {
189
- retryLimit: 2,
190
- leaseTime: 2,
191
- dlqAfterMaxRetries: true
192
- });
193
-
194
- // Configure DLQ
195
- await client.queue(dlq, {
196
- priority: 1 // Lower priority for DLQ
197
- });
198
-
199
- // Push messages that will fail
200
- const problematicMessages = Array.from({ length: 3 }, (_, i) => ({
201
- id: `problem-${i}`,
202
- shouldFail: true,
203
- data: 'This will fail processing'
204
- }));
205
-
206
- await client.push(mainQueue, problematicMessages);
207
-
208
- // Process messages with simulated failures
209
- let dlqCount = 0;
210
- for (let attempt = 0; attempt < 10; attempt++) {
211
- const messages = [];
212
- for await (const msg of client.take(mainQueue, { limit: 10 })) {
213
- messages.push(msg);
214
- }
215
-
216
- if (messages.length === 0) break;
217
-
218
- for (const msg of messages) {
219
- if (msg.data.shouldFail) {
220
- // Check if message is in DLQ
221
- const retryCheck = await dbPool.query(`
222
- SELECT dlq.retry_count
223
- FROM queen.dead_letter_queue dlq
224
- JOIN queen.messages m ON dlq.message_id = m.id
225
- WHERE m.transaction_id = $1 AND dlq.consumer_group = '__QUEUE_MODE__'
226
- `, [msg.transactionId]);
227
-
228
- const retryCount = retryCheck.rows[0]?.retry_count || 0;
229
-
230
- if (retryCount >= 2) {
231
- // Move to DLQ manually (since we're simulating)
232
- await client.push(dlq, {
233
- ...msg.data,
234
- originalQueue: mainQueue,
235
- failureReason: 'Max retries exceeded',
236
- movedToDlqAt: Date.now()
237
- });
238
- dlqCount++;
239
- // Mark as completed to remove from main queue
240
- await client.ack(msg);
241
- } else {
242
- // Fail the message
243
- await client.ack(msg, false, { error: 'Simulated failure' });
244
- }
245
- } else {
246
- await client.ack(msg);
247
- }
248
- }
249
-
250
- await sleep(2500); // Wait for lease expiration
251
- }
252
-
253
- // Verify messages in DLQ
254
- const dlqMessages = [];
255
- for await (const msg of client.take(dlq, { limit: 10 })) {
256
- dlqMessages.push(msg);
257
- await client.ack(msg);
258
- }
259
-
260
- if (dlqMessages.length < dlqCount) {
261
- throw new Error(`Expected at least ${dlqCount} messages in DLQ`);
262
- }
263
-
264
- passTest(`Dead letter queue pattern: ${dlqCount} messages moved to DLQ`);
265
- } catch (error) {
266
- failTest(error);
267
- }
268
- }
269
-
270
- /**
271
- * Test: Circuit breaker pattern
272
- */
273
- export async function testCircuitBreaker(client) {
274
- startTest('Circuit Breaker Pattern', 'pattern');
275
-
276
- try {
277
- const queue = 'pattern-circuit-breaker';
278
-
279
- // Configure queue
280
- await client.queue(queue, {});
281
-
282
- const circuitBreaker = {
283
- state: 'CLOSED', // CLOSED, OPEN, HALF_OPEN
284
- failureCount: 0,
285
- successCount: 0,
286
- failureThreshold: 3,
287
- successThreshold: 2,
288
- timeout: 2000,
289
- lastFailureTime: null
290
- };
291
-
292
- // Push messages
293
- const messages = Array.from({ length: 20 }, (_, i) => ({
294
- id: i,
295
- shouldFail: i < 10 // First 10 will fail
296
- }));
297
-
298
- await client.push(queue, messages);
299
-
300
- // Process with circuit breaker
301
- let processedCount = 0;
302
- const maxAttempts = 30;
303
-
304
- for (let attempt = 0; attempt < maxAttempts && processedCount < messages.length; attempt++) {
305
- // Check circuit breaker state
306
- if (circuitBreaker.state === 'OPEN') {
307
- const timeSinceFailure = Date.now() - circuitBreaker.lastFailureTime;
308
- if (timeSinceFailure >= circuitBreaker.timeout) {
309
- circuitBreaker.state = 'HALF_OPEN';
310
- circuitBreaker.failureCount = 0;
311
- circuitBreaker.successCount = 0;
312
- log('Circuit breaker: OPEN → HALF_OPEN', 'warning');
313
- } else {
314
- log('Circuit breaker is OPEN, waiting...', 'warning');
315
- await sleep(500);
316
- continue;
317
- }
318
- }
319
-
320
- // Try to process message
321
- for await (const msg of client.take(queue, { limit: 1 })) {
322
- try {
323
- if (msg.data.shouldFail && circuitBreaker.state !== 'OPEN') {
324
- throw new Error('Simulated failure');
325
- }
326
-
327
- // Success
328
- await client.ack(msg);
329
- processedCount++;
330
-
331
- if (circuitBreaker.state === 'HALF_OPEN') {
332
- circuitBreaker.successCount++;
333
- if (circuitBreaker.successCount >= circuitBreaker.successThreshold) {
334
- circuitBreaker.state = 'CLOSED';
335
- circuitBreaker.failureCount = 0;
336
- log('Circuit breaker: HALF_OPEN → CLOSED', 'success');
337
- }
338
- }
339
- } catch (error) {
340
- // Failure
341
- await client.ack(msg, false, { error: error.message });
342
- circuitBreaker.failureCount++;
343
- circuitBreaker.lastFailureTime = Date.now();
344
-
345
- if (circuitBreaker.state === 'CLOSED' &&
346
- circuitBreaker.failureCount >= circuitBreaker.failureThreshold) {
347
- circuitBreaker.state = 'OPEN';
348
- log('Circuit breaker: CLOSED → OPEN (threshold reached)', 'error');
349
- } else if (circuitBreaker.state === 'HALF_OPEN') {
350
- circuitBreaker.state = 'OPEN';
351
- log('Circuit breaker: HALF_OPEN → OPEN (failure during recovery)', 'error');
352
- }
353
- }
354
- }
355
-
356
- await sleep(100);
357
- }
358
-
359
- log(`Circuit breaker results: ${processedCount - circuitBreaker.failureCount} success, ${circuitBreaker.failureCount} failed`, 'info');
360
- passTest('Circuit breaker pattern implemented successfully');
361
- } catch (error) {
362
- failTest(error);
363
- }
364
- }
365
-
366
- /**
367
- * Test: Complex priority scenarios
368
- */
369
- export async function testComplexPriorityScenarios(client) {
370
- startTest('Complex Priority Scenarios', 'priority');
371
-
372
- try {
373
- const queue = 'test-complex-priority';
374
-
375
- // Push messages with different priorities
376
- const priorities = [100, 50, 75, 1, 10, 90, 30, 60, 5, 80];
377
- const messages = priorities.map((priority, index) => ({
378
- id: index,
379
- priority,
380
- data: `Priority ${priority} message`
381
- }));
382
-
383
- // Configure queue with priority
384
- await client.queue(queue, {
385
- priority: 50
386
- });
387
-
388
- // Push messages in random priority order
389
- await client.push(queue, messages);
390
- await sleep(100);
391
-
392
- // Take all messages - should come in FIFO order within partition
393
- const received = [];
394
- for await (const msg of client.take(queue, { limit: messages.length })) {
395
- received.push(msg);
396
- await client.ack(msg);
397
- }
398
-
399
- if (received.length !== messages.length) {
400
- throw new Error(`Expected ${messages.length} messages, got ${received.length}`);
401
- }
402
-
403
- // Calculate average priority of first vs last 3 messages
404
- const firstThree = received.slice(0, 3).map(m => m.data.priority);
405
- const lastThree = received.slice(-3).map(m => m.data.priority);
406
-
407
- const avgFirst = firstThree.reduce((a, b) => a + b, 0) / 3;
408
- const avgLast = lastThree.reduce((a, b) => a + b, 0) / 3;
409
-
410
- log(`First 3 messages avg priority: ${avgFirst.toFixed(0)}`, 'info');
411
- log(`Last 3 messages avg priority: ${avgLast.toFixed(0)}`, 'info');
412
-
413
- passTest('Complex priority scenarios handled correctly');
414
- } catch (error) {
415
- failTest(error);
416
- }
417
- }
418
-
419
- /**
420
- * Test: Dynamic priority adjustment
421
- */
422
- export async function testDynamicPriorityAdjustment(client) {
423
- startTest('Dynamic Priority Adjustment', 'priority');
424
-
425
- try {
426
- const queue = 'test-dynamic-priority';
427
-
428
- // Start with low priority
429
- await client.queue(queue, {
430
- priority: 1
431
- });
432
-
433
- // Push some messages
434
- await client.push(queue, {
435
- phase: 'low-priority',
436
- timestamp: Date.now()
437
- });
438
-
439
- // Increase priority
440
- await client.queue(queue, {
441
- priority: 100
442
- });
443
-
444
- // Push more messages
445
- await client.push(queue, {
446
- phase: 'high-priority',
447
- timestamp: Date.now()
448
- });
449
-
450
- // Verify configuration change
451
- const queueInfo = await dbPool.query(
452
- 'SELECT priority FROM queen.queues WHERE name = $1',
453
- [queue]
454
- );
455
-
456
- if (queueInfo.rows[0].priority !== 100) {
457
- throw new Error('Priority not updated correctly');
458
- }
459
-
460
- passTest('Dynamic priority adjustment works');
461
- } catch (error) {
462
- failTest(error);
463
- }
464
- }
465
-
466
- /**
467
- * Test: Saga pattern (distributed transactions)
468
- */
469
- export async function testSagaPattern(client) {
470
- startTest('Saga Pattern (Distributed Transactions)', 'pattern');
471
-
472
- try {
473
- const sagaSteps = [
474
- 'pattern-saga-order',
475
- 'pattern-saga-payment',
476
- 'pattern-saga-inventory',
477
- 'pattern-saga-shipping'
478
- ];
479
-
480
- const compensationSteps = [
481
- 'pattern-saga-cancel-shipping',
482
- 'pattern-saga-restore-inventory',
483
- 'pattern-saga-refund-payment',
484
- 'pattern-saga-cancel-order'
485
- ];
486
-
487
- // Configure all saga queues
488
- for (const queue of [...sagaSteps, ...compensationSteps]) {
489
- await client.queue(queue, {});
490
- }
491
-
492
- const sagaId = crypto.randomBytes(16).toString('hex');
493
- const sagaData = {
494
- id: sagaId,
495
- orderId: 'ORDER-123',
496
- amount: 100,
497
- items: ['item1', 'item2'],
498
- status: 'started'
499
- };
500
-
501
- // Execute saga steps
502
- let currentData = { ...sagaData };
503
- let failedAtStep = -1;
504
-
505
- for (let i = 0; i < sagaSteps.length; i++) {
506
- const step = sagaSteps[i];
507
-
508
- // Push to step queue
509
- await client.push(step, {
510
- ...currentData,
511
- step: i,
512
- stepName: step
513
- });
514
-
515
- // Process step
516
- let stepMessage = null;
517
- for await (const msg of client.take(step, { limit: 1 })) {
518
- stepMessage = msg;
519
- }
520
-
521
- if (stepMessage) {
522
- // Simulate failure at payment step
523
- if (step === 'pattern-saga-payment' && Math.random() > 0.5) {
524
- failedAtStep = i;
525
- await client.ack(stepMessage, false, { error: 'Payment failed' });
526
- break;
527
- }
528
-
529
- // Step succeeded
530
- currentData = {
531
- ...stepMessage.data,
532
- [`${step}_completed`]: true
533
- };
534
-
535
- await client.ack(stepMessage);
536
- }
537
- }
538
-
539
- // If saga failed, execute compensation
540
- if (failedAtStep >= 0) {
541
- log('Saga failed, starting compensation...', 'warning');
542
-
543
- for (let i = failedAtStep; i >= 0; i--) {
544
- const compensationStep = compensationSteps[compensationSteps.length - 1 - i];
545
-
546
- await client.push(compensationStep, {
547
- ...currentData,
548
- compensationStep: i,
549
- compensationReason: 'Saga rollback'
550
- });
551
-
552
- for await (const msg of client.take(compensationStep, { limit: 1 })) {
553
- await client.ack(msg);
554
- }
555
- }
556
- }
557
-
558
- passTest('Saga pattern with compensation executed successfully');
559
- } catch (error) {
560
- failTest(error);
561
- }
562
- }
563
-
564
- /**
565
- * Test: Rate limiting and throttling
566
- */
567
- export async function testRateLimiting(client) {
568
- startTest('Rate Limiting and Throttling', 'pattern');
569
-
570
- try {
571
- const queue = 'pattern-rate-limited';
572
- const rateLimit = 5; // 5 messages per second
573
-
574
- // Configure queue
575
- await client.queue(queue, {
576
- windowBuffer: 1 // 1 second window
577
- });
578
-
579
- // Push burst of messages
580
- const burstSize = 20;
581
- const messages = Array.from({ length: burstSize }, (_, i) => ({
582
- id: i,
583
- timestamp: Date.now()
584
- }));
585
-
586
- await client.push(queue, messages);
587
-
588
- // Implement rate-limited consumer
589
- const processedMessages = [];
590
- const startTime = Date.now();
591
- let lastWindowStart = startTime;
592
- let windowMessageCount = 0;
593
-
594
- while (processedMessages.length < burstSize) {
595
- const currentTime = Date.now();
596
-
597
- // Reset window if needed
598
- if (currentTime - lastWindowStart >= 1000) {
599
- lastWindowStart = currentTime;
600
- windowMessageCount = 0;
601
- }
602
-
603
- // Check if we can process more messages
604
- if (windowMessageCount < rateLimit) {
605
- const batchSize = Math.min(
606
- rateLimit - windowMessageCount,
607
- burstSize - processedMessages.length
608
- );
609
-
610
- for await (const msg of client.take(queue, { limit: batchSize })) {
611
- processedMessages.push({
612
- id: msg.data.id,
613
- processedAt: Date.now() - startTime
614
- });
615
- windowMessageCount++;
616
- await client.ack(msg);
617
- }
618
- } else {
619
- // Wait for next window
620
- await sleep(Math.max(0, 1000 - (currentTime - lastWindowStart)));
621
- }
622
- }
623
-
624
- const totalTime = Date.now() - startTime;
625
- const actualRate = (burstSize / (totalTime / 1000)).toFixed(2);
626
-
627
- log(`Processed ${burstSize} messages at ${actualRate} msg/sec (limit: ${rateLimit})`, 'info');
628
-
629
- // Allow up to 40% variance due to timing variations
630
- if (actualRate > rateLimit * 1.4) {
631
- throw new Error(`Rate limit exceeded: ${actualRate} > ${rateLimit * 1.4}`);
632
- }
633
-
634
- passTest(`Rate limiting enforced: ${actualRate} msg/sec`);
635
- } catch (error) {
636
- failTest(error);
637
- }
638
- }
639
-
640
- /**
641
- * Test: Time-based batch processing
642
- */
643
- export async function testTimeBatchProcessing(client) {
644
- startTest('Time-Based Batch Processing', 'pattern');
645
-
646
- try {
647
- const queue = 'pattern-time-batch';
648
-
649
- // Configure queue
650
- await client.queue(queue, {});
651
-
652
- const batchWindow = 2000; // 2 seconds
653
- const minBatchSize = 3;
654
- const maxBatchSize = 10;
655
-
656
- // Push messages over time
657
- const messageCount = 20;
658
- for (let i = 0; i < messageCount; i++) {
659
- await client.push(queue, {
660
- id: i,
661
- timestamp: Date.now()
662
- });
663
-
664
- // Stagger message arrival
665
- if (i % 5 === 0) {
666
- await sleep(500);
667
- }
668
- }
669
-
670
- // Process in time-based batches
671
- const batches = [];
672
- let currentBatch = [];
673
- let batchStartTime = Date.now();
674
- let totalProcessed = 0;
675
-
676
- while (totalProcessed < messageCount) {
677
- const currentTime = Date.now();
678
- const batchAge = currentTime - batchStartTime;
679
-
680
- // Check if batch should be processed
681
- if (currentBatch.length >= maxBatchSize ||
682
- (batchAge >= batchWindow && currentBatch.length >= minBatchSize)) {
683
-
684
- batches.push({
685
- size: currentBatch.length,
686
- duration: batchAge,
687
- messages: [...currentBatch]
688
- });
689
-
690
- // Acknowledge batch
691
- for (const msg of currentBatch) {
692
- await client.ack(msg);
693
- }
694
-
695
- totalProcessed += currentBatch.length;
696
- currentBatch = [];
697
- batchStartTime = Date.now();
698
- }
699
-
700
- // Try to add more messages to batch
701
- const remaining = Math.min(maxBatchSize - currentBatch.length, messageCount - totalProcessed);
702
- if (remaining > 0) {
703
- let gotMessages = false;
704
- for await (const msg of client.take(queue, { limit: remaining })) {
705
- currentBatch.push(msg);
706
- gotMessages = true;
707
- }
708
-
709
- // If we didn't get any messages
710
- if (!gotMessages) {
711
- // If we have messages in the batch and window expired, process them
712
- if (currentBatch.length > 0 && batchAge >= batchWindow) {
713
- batches.push({
714
- size: currentBatch.length,
715
- duration: batchAge,
716
- messages: [...currentBatch]
717
- });
718
-
719
- for (const msg of currentBatch) {
720
- await client.ack(msg);
721
- }
722
-
723
- totalProcessed += currentBatch.length;
724
- currentBatch = [];
725
- batchStartTime = Date.now();
726
- } else {
727
- // Wait a bit before retrying
728
- await sleep(100);
729
- }
730
- }
731
- }
732
-
733
- // Safety valve: if we're stuck, break out
734
- if (totalProcessed === 0 && batches.length === 0 && currentBatch.length === 0) {
735
- log('No messages available, breaking out of batch processing', 'warning');
736
- break;
737
- }
738
- }
739
-
740
- // Process any remaining messages in current batch
741
- if (currentBatch.length > 0) {
742
- batches.push({
743
- size: currentBatch.length,
744
- duration: Date.now() - batchStartTime,
745
- messages: [...currentBatch]
746
- });
747
-
748
- for (const msg of currentBatch) {
749
- await client.ack(msg);
750
- }
751
-
752
- totalProcessed += currentBatch.length;
753
- }
754
-
755
- log(`Created ${batches.length} time-based batches`, 'info');
756
- batches.forEach(batch => {
757
- log(` Batch: ${batch.size} messages in ${batch.duration}ms`, 'info');
758
- });
759
-
760
- passTest(`Time-based batching created ${batches.length} batches`);
761
- } catch (error) {
762
- failTest(error);
763
- }
764
- }
765
-
766
- /**
767
- * Test: Event sourcing with replay
768
- */
769
- export async function testEventSourcing(client) {
770
- startTest('Event Sourcing with Replay', 'pattern');
771
-
772
- try {
773
- const eventQueue = 'pattern-event-store';
774
- const snapshotQueue = 'pattern-snapshots';
775
-
776
- // Configure queues
777
- await client.queue(eventQueue, {
778
- retentionEnabled: true,
779
- retentionSeconds: 86400 // Keep events for 24 hours
780
- });
781
-
782
- await client.queue(snapshotQueue, {});
783
-
784
- // Generate events
785
- const entityId = crypto.randomBytes(8).toString('hex');
786
- const events = [
787
- { type: 'created', entityId, data: { name: 'Test Entity' }, version: 1 },
788
- { type: 'updated', entityId, data: { name: 'Updated Entity' }, version: 2 },
789
- { type: 'statusChanged', entityId, data: { status: 'active' }, version: 3 },
790
- { type: 'updated', entityId, data: { name: 'Final Entity' }, version: 4 },
791
- { type: 'statusChanged', entityId, data: { status: 'completed' }, version: 5 }
792
- ];
793
-
794
- // Store events
795
- for (const event of events) {
796
- await client.push(eventQueue, {
797
- ...event,
798
- timestamp: Date.now()
799
- });
800
- }
801
-
802
- // Replay events to rebuild state
803
- const rebuiltState = {
804
- entityId,
805
- version: 0,
806
- data: {}
807
- };
808
-
809
- const allEvents = [];
810
- for await (const msg of client.take(eventQueue, { limit: 100 })) {
811
- allEvents.push(msg);
812
- }
813
-
814
- if (allEvents.length > 0) {
815
- // Sort by version to ensure correct order
816
- allEvents.sort((a, b) => a.data.version - b.data.version);
817
-
818
- for (const msg of allEvents) {
819
- const event = msg.data;
820
-
821
- switch (event.type) {
822
- case 'created':
823
- rebuiltState.data = event.data;
824
- break;
825
- case 'updated':
826
- rebuiltState.data = { ...rebuiltState.data, ...event.data };
827
- break;
828
- case 'statusChanged':
829
- rebuiltState.data.status = event.data.status;
830
- break;
831
- }
832
-
833
- rebuiltState.version = event.version;
834
-
835
- // Don't ack events - we want to keep them for replay
836
- }
837
- }
838
-
839
- // Create snapshot after replay
840
- if (rebuiltState.version === events.length) {
841
- await client.push(snapshotQueue, {
842
- ...rebuiltState,
843
- snapshotTime: Date.now()
844
- });
845
- }
846
-
847
- passTest(`Event sourcing: ${events.length} events replayed, state rebuilt`);
848
- } catch (error) {
849
- failTest(error);
850
- }
851
- }
852
-
853
- /**
854
- * Test: Queue metrics and monitoring
855
- */
856
- export async function testQueueMetrics(client) {
857
- startTest('Queue Metrics and Monitoring', 'pattern');
858
-
859
- try {
860
- const queue = 'pattern-metrics';
861
- const messageCount = 50;
862
-
863
- // Configure queue
864
- await client.queue(queue, {
865
- leaseTime: 10
866
- });
867
-
868
- // Track metrics
869
- const metrics = {
870
- published: 0,
871
- consumed: 0,
872
- completed: 0,
873
- failed: 0,
874
- processingTimes: [],
875
- throughput: 0
876
- };
877
-
878
- const startTime = Date.now();
879
-
880
- // Publish messages
881
- const messages = Array.from({ length: messageCount }, (_, i) => ({
882
- id: i,
883
- shouldFail: Math.random() > 0.7, // 30% failure rate
884
- processingTime: Math.floor(Math.random() * 100) // 0-100ms
885
- }));
886
-
887
- await client.push(queue, messages);
888
- metrics.published = messageCount;
889
-
890
- // Consume and process
891
- while (metrics.consumed < messageCount) {
892
- for await (const msg of client.take(queue, { limit: 10 })) {
893
- metrics.consumed++;
894
- const processStart = Date.now();
895
-
896
- // Simulate processing
897
- await sleep(msg.data.processingTime);
898
-
899
- if (msg.data.shouldFail) {
900
- await client.ack(msg, false, { error: 'Simulated failure' });
901
- metrics.failed++;
902
- } else {
903
- await client.ack(msg);
904
- metrics.completed++;
905
- }
906
-
907
- metrics.processingTimes.push(Date.now() - processStart);
908
- }
909
-
910
- if (metrics.consumed < messageCount) {
911
- await sleep(100);
912
- }
913
- }
914
-
915
- const totalTime = Date.now() - startTime;
916
- metrics.throughput = (messageCount / (totalTime / 1000)).toFixed(2);
917
-
918
- // Calculate additional metrics
919
- const avgProcessingTime = metrics.processingTimes.reduce((a, b) => a + b, 0) / metrics.processingTimes.length;
920
-
921
- log('Queue Metrics:', 'info');
922
- log(` Published: ${metrics.published}`, 'info');
923
- log(` Consumed: ${metrics.consumed}`, 'info');
924
- log(` Completed: ${metrics.completed}`, 'info');
925
- log(` Failed: ${metrics.failed}`, 'info');
926
- log(` Avg Processing: ${avgProcessingTime.toFixed(2)}ms`, 'info');
927
- log(` Throughput: ${metrics.throughput} msg/sec`, 'info');
928
-
929
- passTest('Queue metrics collected successfully');
930
- } catch (error) {
931
- failTest(error);
932
- }
933
- }
934
-
935
- /**
936
- * Test: Multiple Consumers - Single Partition Message Ordering
937
- * Verifies that multiple consumers maintain message ordering
938
- */
939
- export async function testMultipleConsumersSinglePartitionOrdering(client) {
940
- startTest('Multiple Consumers - Single Partition Message Ordering', 'pattern');
941
-
942
- try {
943
- const queue = 'test-multi-consumer-ordering';
944
- const partition = 'single-partition';
945
- const numMessages = 999;
946
- const numConsumers = 10;
947
- const batchSize = 50;
948
-
949
- // Configure queue
950
- await client.queue(queue, {
951
- leaseTime: 30,
952
- retryLimit: 3
953
- });
954
-
955
- log(`Pushing ${numMessages} messages to queue '${queue}', partition '${partition}'`);
956
-
957
- // Push messages with sequential order identifiers
958
- const pushStartTime = Date.now();
959
- const items = Array.from({ length: numMessages }, (_, i) => ({
960
- sequenceNumber: i,
961
- data: `Message ${i}`,
962
- timestamp: Date.now()
963
- }));
964
-
965
- // Push in batches
966
- const pushBatchSize = 100;
967
- for (let i = 0; i < items.length; i += pushBatchSize) {
968
- const batch = items.slice(i, Math.min(i + pushBatchSize, items.length));
969
- await client.push(`${queue}/${partition}`, batch);
970
- }
971
-
972
- const pushTime = Date.now() - pushStartTime;
973
- log(`Pushed ${numMessages} messages in ${pushTime}ms`);
974
-
975
- await sleep(500);
976
-
977
- // Verify messages are in the queue
978
- const messageCount = await getMessageCount(queue, partition);
979
- if (messageCount !== numMessages) {
980
- throw new Error(`Expected ${numMessages} messages in queue, but found ${messageCount}`);
981
- }
982
-
983
- log(`Starting ${numConsumers} consumers to process messages`);
984
-
985
- // Track all consumed messages
986
- const consumedMessages = [];
987
- const consumerPromises = [];
988
-
989
- // Create multiple consumers
990
- for (let consumerId = 0; consumerId < numConsumers; consumerId++) {
991
- const consumerPromise = (async () => {
992
- const consumerMessages = [];
993
- let consecutiveEmptyPolls = 0;
994
- const maxEmptyPolls = 3;
995
-
996
- while (consecutiveEmptyPolls < maxEmptyPolls) {
997
- try {
998
- let gotMessages = false;
999
- for await (const msg of client.take(`${queue}/${partition}`, {
1000
- limit: batchSize,
1001
- batch: batchSize
1002
- })) {
1003
- const sequenceNumber = msg.data.sequenceNumber;
1004
- consumerMessages.push({
1005
- consumerId,
1006
- sequenceNumber,
1007
- timestamp: Date.now(),
1008
- transactionId: msg.transactionId
1009
- });
1010
-
1011
- await client.ack(msg);
1012
- gotMessages = true;
1013
- }
1014
-
1015
- if (!gotMessages) {
1016
- consecutiveEmptyPolls++;
1017
- await sleep(100);
1018
- } else {
1019
- consecutiveEmptyPolls = 0;
1020
- log(`Consumer ${consumerId} processed batch (total: ${consumerMessages.length})`);
1021
- }
1022
- } catch (error) {
1023
- log(`Consumer ${consumerId} error: ${error.message}`, 'warning');
1024
- await sleep(500);
1025
- }
1026
- }
1027
-
1028
- if (consumerMessages.length > 0) {
1029
- log(`Consumer ${consumerId} completed with ${consumerMessages.length} total messages`);
1030
- }
1031
-
1032
- return consumerMessages;
1033
- })();
1034
-
1035
- consumerPromises.push(consumerPromise);
1036
-
1037
- // Stagger consumer starts
1038
- await sleep(50);
1039
- }
1040
-
1041
- // Wait for all consumers to complete
1042
- log('Waiting for all consumers to complete processing...');
1043
- const consumerResults = await Promise.all(consumerPromises);
1044
-
1045
- // Combine all consumed messages
1046
- for (const messages of consumerResults) {
1047
- consumedMessages.push(...messages);
1048
- }
1049
-
1050
- // Sort by sequence number
1051
- consumedMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
1052
-
1053
- log(`Total messages consumed: ${consumedMessages.length}`);
1054
-
1055
- // Verify all messages were consumed exactly once
1056
- if (consumedMessages.length !== numMessages) {
1057
- throw new Error(`Expected ${numMessages} messages to be consumed, but got ${consumedMessages.length}`);
1058
- }
1059
-
1060
- // Verify no duplicates
1061
- const sequenceNumbers = new Set(consumedMessages.map(m => m.sequenceNumber));
1062
- if (sequenceNumbers.size !== numMessages) {
1063
- throw new Error(`Found duplicate messages: expected ${numMessages} unique sequences, got ${sequenceNumbers.size}`);
1064
- }
1065
-
1066
- // Verify sequential ordering
1067
- for (let i = 0; i < numMessages; i++) {
1068
- if (consumedMessages[i].sequenceNumber !== i) {
1069
- throw new Error(`Message ordering violation at position ${i}: expected sequence ${i}, got ${consumedMessages[i].sequenceNumber}`);
1070
- }
1071
- }
1072
-
1073
- // Analyze consumer distribution
1074
- const consumerStats = {};
1075
- for (const msg of consumedMessages) {
1076
- consumerStats[msg.consumerId] = (consumerStats[msg.consumerId] || 0) + 1;
1077
- }
1078
-
1079
- log('Consumer distribution:', 'info');
1080
- for (const [consumerId, count] of Object.entries(consumerStats)) {
1081
- log(` Consumer ${consumerId}: ${count} messages (${(count / numMessages * 100).toFixed(1)}%)`, 'info');
1082
- }
1083
-
1084
- passTest(`${numConsumers} consumers successfully processed ${numMessages} messages from single partition with correct ordering`);
1085
- } catch (error) {
1086
- failTest(error);
1087
- }
1088
- }
1089
-
1090
- /**
1091
- * Test: Message deduplication
1092
- */
1093
- export async function testMessageDeduplication(client) {
1094
- startTest('Message Deduplication', 'pattern');
1095
-
1096
- try {
1097
- const queue = 'pattern-dedup';
1098
-
1099
- // Configure queue
1100
- await client.queue(queue, {});
1101
-
1102
- const deduplicationWindow = new Map();
1103
-
1104
- // Push messages with some duplicates
1105
- const messages = [
1106
- { id: 'msg-1', data: 'First' },
1107
- { id: 'msg-2', data: 'Second' },
1108
- { id: 'msg-1', data: 'First duplicate' }, // Duplicate
1109
- { id: 'msg-3', data: 'Third' },
1110
- { id: 'msg-2', data: 'Second duplicate' }, // Duplicate
1111
- { id: 'msg-4', data: 'Fourth' }
1112
- ];
1113
-
1114
- await client.push(queue, messages);
1115
-
1116
- // Process with deduplication
1117
- const uniqueMessages = [];
1118
- for await (const msg of client.take(queue, { limit: messages.length })) {
1119
- const messageId = msg.data.id;
1120
-
1121
- // Check for duplicate
1122
- if (!deduplicationWindow.has(messageId)) {
1123
- deduplicationWindow.set(messageId, Date.now());
1124
- uniqueMessages.push(msg);
1125
- } else {
1126
- log(`Duplicate detected: ${messageId}`, 'warning');
1127
- }
1128
-
1129
- await client.ack(msg);
1130
- }
1131
-
1132
- passTest(`Deduplication successful: ${messages.length} messages → ${uniqueMessages.length} unique`);
1133
- } catch (error) {
1134
- failTest(error);
1135
- }
1136
- }
1137
-