queen-mq 0.1.0 → 0.1.2
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/API.md +862 -752
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +1705 -1051
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-complete-client.js +260 -0
- package/examples/test-dashboard-api.js +200 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +17 -4
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +300 -31
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +361 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +561 -0
- package/src/test/enterprise-tests.js +637 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
- package/src/routes/analytics.js +0 -812
|
@@ -0,0 +1,1137 @@
|
|
|
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.payload.priority);
|
|
405
|
+
const lastThree = received.slice(-3).map(m => m.payload.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.payload.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
|
+
|