queen-mq 0.1.1 → 0.1.3
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 +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/PROGRAMMATIC_SERVER.md +190 -0
- package/README.md +303 -53
- 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/programmatic-server.js +58 -0
- package/examples/test-cache-multi-server.js +2 -2
- package/examples/test-connection-recovery.js +66 -0
- package/examples/test-dashboard-api.js +200 -0
- package/package.json +4 -2
- 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 +219 -17
- package/src/client/index.js +4 -3
- 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 +852 -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 +704 -642
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/services/startupSync.js +1 -1
- package/src/test/advanced-pattern-tests.js +6 -6
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/core-tests.js +110 -0
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/test-new.js +3 -1
- package/src/test/test.js +1 -1
- package/src/test/utils.js +1 -1
- package/src/test/window-buffer-test.js +114 -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 +101 -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 +363 -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/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/src/client/queenClient.js +0 -513
- package/src/routes/analytics.js +0 -812
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test connection recovery during take operations
|
|
3
|
+
* This demonstrates how the client handles server disconnections gracefully
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Queen } from '../src/client/index.js';
|
|
7
|
+
|
|
8
|
+
async function testConnectionRecovery() {
|
|
9
|
+
const client = new Queen({
|
|
10
|
+
baseUrl: 'http://localhost:6632',
|
|
11
|
+
retryDelay: 1000, // 1 second base retry delay
|
|
12
|
+
retryAttempts: 3
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Configure a test queue
|
|
17
|
+
await client.queue('test-recovery-queue');
|
|
18
|
+
console.log('Queue configured');
|
|
19
|
+
|
|
20
|
+
// Push some test messages
|
|
21
|
+
await client.push('test-recovery-queue', [
|
|
22
|
+
{ message: 'Test message 1' },
|
|
23
|
+
{ message: 'Test message 2' },
|
|
24
|
+
{ message: 'Test message 3' }
|
|
25
|
+
]);
|
|
26
|
+
console.log('Pushed 3 test messages');
|
|
27
|
+
|
|
28
|
+
console.log('\nStarting consumer...');
|
|
29
|
+
console.log('To test: Stop the server after receiving some messages');
|
|
30
|
+
console.log('Expected behavior: Client will retry with exponential backoff indefinitely (max 30s between retries)');
|
|
31
|
+
console.log(' When server comes back online, it will automatically reconnect\n');
|
|
32
|
+
|
|
33
|
+
let messageCount = 0;
|
|
34
|
+
|
|
35
|
+
// Consume messages with takeBatch
|
|
36
|
+
for await (const batch of client.takeBatch('test-recovery-queue', {
|
|
37
|
+
batch: 10,
|
|
38
|
+
wait: true,
|
|
39
|
+
timeout: 5000
|
|
40
|
+
})) {
|
|
41
|
+
console.log(`\nReceived batch of ${batch.length} messages:`);
|
|
42
|
+
|
|
43
|
+
for (const message of batch) {
|
|
44
|
+
messageCount++;
|
|
45
|
+
console.log(` ${messageCount}. ${JSON.stringify(message.payload)}`);
|
|
46
|
+
|
|
47
|
+
// Acknowledge the message
|
|
48
|
+
await client.ack(message, true);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('\nConsumer loop completed');
|
|
53
|
+
console.log(`Total messages processed: ${messageCount}`);
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('\nError in consumer:', error.message);
|
|
57
|
+
console.error('Stack:', error.stack);
|
|
58
|
+
} finally {
|
|
59
|
+
await client.close();
|
|
60
|
+
console.log('\nClient closed');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('=== Connection Recovery Test ===\n');
|
|
65
|
+
testConnectionRecovery().catch(console.error);
|
|
66
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test script for the new Dashboard Status API endpoints
|
|
3
|
+
*
|
|
4
|
+
* This script demonstrates all 5 new endpoints:
|
|
5
|
+
* 1. GET /api/v1/status - Dashboard overview
|
|
6
|
+
* 2. GET /api/v1/status/queues - List queues with stats
|
|
7
|
+
* 3. GET /api/v1/status/queues/:queue - Queue detail
|
|
8
|
+
* 4. GET /api/v1/status/queues/:queue/messages - Queue messages
|
|
9
|
+
* 5. GET /api/v1/status/analytics - Advanced analytics
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createQueenClient } from '../src/client/queenClient.js';
|
|
13
|
+
|
|
14
|
+
const BASE_URL = process.env.QUEEN_URL || 'http://localhost:3000';
|
|
15
|
+
|
|
16
|
+
// Create client
|
|
17
|
+
const client = createQueenClient({ baseUrl: BASE_URL });
|
|
18
|
+
|
|
19
|
+
// Helper to make GET requests
|
|
20
|
+
async function get(path) {
|
|
21
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = await response.text();
|
|
30
|
+
throw new Error(`HTTP ${response.status}: ${error}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await response.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to format JSON output
|
|
37
|
+
function printJson(label, data) {
|
|
38
|
+
console.log(`\n${'='.repeat(80)}`);
|
|
39
|
+
console.log(`📊 ${label}`);
|
|
40
|
+
console.log('='.repeat(80));
|
|
41
|
+
console.log(JSON.stringify(data, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runTests() {
|
|
45
|
+
console.log('🚀 Testing Queen Dashboard Status API\n');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// First, let's create some test data
|
|
49
|
+
console.log('📝 Setting up test data...');
|
|
50
|
+
|
|
51
|
+
// Configure a test queue
|
|
52
|
+
await client.configure({
|
|
53
|
+
queue: 'dashboard-test-queue',
|
|
54
|
+
namespace: 'test-namespace',
|
|
55
|
+
task: 'test-task',
|
|
56
|
+
partitions: 3
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Push some test messages
|
|
60
|
+
const messages = [];
|
|
61
|
+
for (let i = 0; i < 20; i++) {
|
|
62
|
+
messages.push({
|
|
63
|
+
queue: 'dashboard-test-queue',
|
|
64
|
+
partition: `partition-${i % 3}`,
|
|
65
|
+
payload: {
|
|
66
|
+
messageNumber: i,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
data: `Test message ${i}`
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await client.pushBatch(messages);
|
|
74
|
+
console.log('✅ Created test queue with 20 messages\n');
|
|
75
|
+
|
|
76
|
+
// Wait a bit for messages to be processed
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
78
|
+
|
|
79
|
+
// Test 1: Dashboard Status
|
|
80
|
+
console.log('🧪 Test 1: Dashboard Status (GET /api/v1/status)');
|
|
81
|
+
const status = await get('/api/v1/status');
|
|
82
|
+
printJson('Dashboard Status', {
|
|
83
|
+
timeRange: status.timeRange,
|
|
84
|
+
queuesCount: status.queues.length,
|
|
85
|
+
messages: status.messages,
|
|
86
|
+
leases: status.leases,
|
|
87
|
+
throughputSamples: status.throughput.length,
|
|
88
|
+
dlqTotal: status.deadLetterQueue.totalMessages
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Test 2: Queues List
|
|
92
|
+
console.log('\n🧪 Test 2: List Queues (GET /api/v1/status/queues)');
|
|
93
|
+
const queues = await get('/api/v1/status/queues?limit=10');
|
|
94
|
+
printJson('Queues List', {
|
|
95
|
+
total: queues.pagination.total,
|
|
96
|
+
queues: queues.queues.map(q => ({
|
|
97
|
+
name: q.name,
|
|
98
|
+
namespace: q.namespace,
|
|
99
|
+
partitions: q.partitions,
|
|
100
|
+
pending: q.messages.pending,
|
|
101
|
+
completed: q.messages.completed
|
|
102
|
+
}))
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Test 3: Queue Detail
|
|
106
|
+
console.log('\n🧪 Test 3: Queue Detail (GET /api/v1/status/queues/:queue)');
|
|
107
|
+
const queueDetail = await get('/api/v1/status/queues/dashboard-test-queue');
|
|
108
|
+
printJson('Queue Detail', {
|
|
109
|
+
queue: queueDetail.queue,
|
|
110
|
+
totals: queueDetail.totals,
|
|
111
|
+
partitionCount: queueDetail.partitions.length,
|
|
112
|
+
partitions: queueDetail.partitions.map(p => ({
|
|
113
|
+
name: p.name,
|
|
114
|
+
messages: p.messages,
|
|
115
|
+
cursor: p.cursor
|
|
116
|
+
}))
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Test 4: Queue Messages
|
|
120
|
+
console.log('\n🧪 Test 4: Queue Messages (GET /api/v1/status/queues/:queue/messages)');
|
|
121
|
+
const messages_result = await get('/api/v1/status/queues/dashboard-test-queue/messages?limit=5');
|
|
122
|
+
printJson('Queue Messages', {
|
|
123
|
+
total: messages_result.pagination.total,
|
|
124
|
+
showing: messages_result.messages.length,
|
|
125
|
+
messages: messages_result.messages.map(m => ({
|
|
126
|
+
transactionId: m.transactionId,
|
|
127
|
+
partition: m.partition,
|
|
128
|
+
status: m.status,
|
|
129
|
+
createdAt: m.createdAt
|
|
130
|
+
}))
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Test 5: Filter by status
|
|
134
|
+
console.log('\n🧪 Test 5: Filter Messages by Status (GET /api/v1/status/queues/:queue/messages?status=pending)');
|
|
135
|
+
const pendingMessages = await get('/api/v1/status/queues/dashboard-test-queue/messages?status=pending&limit=5');
|
|
136
|
+
printJson('Pending Messages', {
|
|
137
|
+
total: pendingMessages.pagination.total,
|
|
138
|
+
messages: pendingMessages.messages.map(m => ({
|
|
139
|
+
transactionId: m.transactionId,
|
|
140
|
+
status: m.status,
|
|
141
|
+
age: m.age
|
|
142
|
+
}))
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Test 6: Analytics (last hour with minute interval)
|
|
146
|
+
console.log('\n🧪 Test 6: Analytics (GET /api/v1/status/analytics?interval=minute)');
|
|
147
|
+
const analytics = await get('/api/v1/status/analytics?interval=minute');
|
|
148
|
+
printJson('Analytics Overview', {
|
|
149
|
+
timeRange: analytics.timeRange,
|
|
150
|
+
throughput: {
|
|
151
|
+
totals: analytics.throughput.totals,
|
|
152
|
+
recentSamples: analytics.throughput.timeSeries.slice(0, 3)
|
|
153
|
+
},
|
|
154
|
+
latency: analytics.latency.overall,
|
|
155
|
+
errorRates: analytics.errorRates.overall,
|
|
156
|
+
currentDepth: analytics.queueDepths.current,
|
|
157
|
+
topQueues: analytics.topQueues.slice(0, 3),
|
|
158
|
+
dlqTotal: analytics.deadLetterQueue.total
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Test 7: Filter by namespace
|
|
162
|
+
console.log('\n🧪 Test 7: Dashboard Status filtered by namespace (GET /api/v1/status?namespace=test-namespace)');
|
|
163
|
+
const statusFiltered = await get('/api/v1/status?namespace=test-namespace');
|
|
164
|
+
printJson('Filtered Status', {
|
|
165
|
+
queuesCount: statusFiltered.queues.length,
|
|
166
|
+
queues: statusFiltered.queues.map(q => q.name),
|
|
167
|
+
messages: statusFiltered.messages
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Test 8: Analytics with time range
|
|
171
|
+
console.log('\n🧪 Test 8: Analytics with custom time range (last 30 minutes)');
|
|
172
|
+
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const analyticsTimeRange = await get(`/api/v1/status/analytics?from=${thirtyMinutesAgo}&to=${now}&interval=minute`);
|
|
175
|
+
printJson('Analytics (Last 30 min)', {
|
|
176
|
+
timeRange: analyticsTimeRange.timeRange,
|
|
177
|
+
throughputSamples: analyticsTimeRange.throughput.timeSeries.length,
|
|
178
|
+
totals: analyticsTimeRange.throughput.totals
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log('\n' + '='.repeat(80));
|
|
182
|
+
console.log('✅ All tests completed successfully!');
|
|
183
|
+
console.log('='.repeat(80));
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('\n❌ Test failed:', error.message);
|
|
187
|
+
console.error(error.stack);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Run tests
|
|
193
|
+
runTests().then(() => {
|
|
194
|
+
console.log('\n✨ Dashboard API test complete!');
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}).catch(error => {
|
|
197
|
+
console.error('Fatal error:', error);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
200
|
+
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "queen-mq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "High-performance message queue backed by PostgreSQL",
|
|
6
6
|
"main": "src/client/index.js",
|
|
7
|
-
|
|
8
7
|
"scripts": {
|
|
9
8
|
"start": "node src/server.js",
|
|
9
|
+
"start:cluster": "node src/cluster-server.js",
|
|
10
10
|
"test:produce": "node test.js produce",
|
|
11
11
|
"test:consume": "node test.js consume",
|
|
12
12
|
"test": "node src/test/test.js",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"init": "node init-db.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"axios": "^1.12.2",
|
|
27
|
+
"chart.js": "^4.5.1",
|
|
26
28
|
"pg": "^8.13.3",
|
|
27
29
|
"uuid": "^13.0.0",
|
|
28
30
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
|
package/server.log
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zsh: command not found: node
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Queen } from '../client/client.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
const QUEUE_NAME = 'benchmark-queue-001';
|
|
5
|
+
const NUMBER_OF_CONSUMERS = 10;
|
|
6
|
+
const BATCH_SIZE = 10;
|
|
7
|
+
const CONSUME_MODE = 'partition';
|
|
8
|
+
|
|
9
|
+
// Global metrics tracking
|
|
10
|
+
const metrics = {
|
|
11
|
+
startTime: null,
|
|
12
|
+
endTime: null,
|
|
13
|
+
totalProcessed: 0,
|
|
14
|
+
consumers: [],
|
|
15
|
+
batchTimes: [],
|
|
16
|
+
ackTimes: [],
|
|
17
|
+
popTimes: [],
|
|
18
|
+
firstMessageTime: null,
|
|
19
|
+
lastMessageTime: null
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function consumer(consumerId, partition) {
|
|
23
|
+
const q = new Queen({
|
|
24
|
+
baseUrls: ['http://localhost:6632'],
|
|
25
|
+
timeout: 30000
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const consumerMetrics = {
|
|
29
|
+
consumerId,
|
|
30
|
+
partition,
|
|
31
|
+
messagesProcessed: 0,
|
|
32
|
+
batchesProcessed: 0,
|
|
33
|
+
batchTimes: [],
|
|
34
|
+
ackTimes: [],
|
|
35
|
+
popTimes: [],
|
|
36
|
+
startTime: Date.now(),
|
|
37
|
+
endTime: null
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const target = CONSUME_MODE === 'partition' ? `${QUEUE_NAME}/${partition}` : `${QUEUE_NAME}`;
|
|
41
|
+
|
|
42
|
+
metrics.consumers.push(consumerMetrics);
|
|
43
|
+
if (target === `${QUEUE_NAME}`) {
|
|
44
|
+
console.log(`[Consumer ${consumerId}] Started, consuming all partitions`);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(`[Consumer ${consumerId}] Started, consuming partition ${partition}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
for await (const messages of q.takeBatch(target, { //`namespace:benchmark`,
|
|
51
|
+
wait: true,
|
|
52
|
+
timeout: 15000,
|
|
53
|
+
batch: BATCH_SIZE,
|
|
54
|
+
idleTimeout: 5000
|
|
55
|
+
})) {
|
|
56
|
+
if (!metrics.firstMessageTime) {
|
|
57
|
+
metrics.firstMessageTime = Date.now();
|
|
58
|
+
}
|
|
59
|
+
metrics.lastMessageTime = Date.now();
|
|
60
|
+
const batchStartTime = Date.now();
|
|
61
|
+
const popTime = batchStartTime - (consumerMetrics.endTime || consumerMetrics.startTime);
|
|
62
|
+
consumerMetrics.popTimes.push(popTime);
|
|
63
|
+
metrics.popTimes.push(popTime);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Validate message ordering
|
|
67
|
+
const partitionLastIds = new Map();
|
|
68
|
+
|
|
69
|
+
for (const message of messages) {
|
|
70
|
+
if (!message.data || typeof message.data.id !== 'number') {
|
|
71
|
+
throw new Error(`Message missing or invalid data.id: ${JSON.stringify(message)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const partition = message.partition || 'default';
|
|
75
|
+
const messageId = message.data.id;
|
|
76
|
+
|
|
77
|
+
if (partitionLastIds.has(partition)) {
|
|
78
|
+
const lastId = partitionLastIds.get(partition);
|
|
79
|
+
if (messageId !== lastId + 1) {
|
|
80
|
+
console.log(message.partition, message);
|
|
81
|
+
throw new Error(`Message ordering violation in partition '${partition}': expected id ${lastId + 1}, got ${messageId}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
partitionLastIds.set(partition, messageId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Acknowledge all messages
|
|
89
|
+
const ackStartTime = Date.now();
|
|
90
|
+
await q.ack(messages);
|
|
91
|
+
const ackTime = Date.now() - ackStartTime;
|
|
92
|
+
consumerMetrics.ackTimes.push(ackTime);
|
|
93
|
+
metrics.ackTimes.push(ackTime);
|
|
94
|
+
|
|
95
|
+
const batchEndTime = Date.now();
|
|
96
|
+
const batchDuration = batchEndTime - batchStartTime;
|
|
97
|
+
|
|
98
|
+
consumerMetrics.messagesProcessed += messages.length;
|
|
99
|
+
consumerMetrics.batchesProcessed++;
|
|
100
|
+
consumerMetrics.batchTimes.push(batchDuration);
|
|
101
|
+
consumerMetrics.endTime = batchEndTime;
|
|
102
|
+
|
|
103
|
+
metrics.totalProcessed += messages.length;
|
|
104
|
+
metrics.batchTimes.push(batchDuration);
|
|
105
|
+
|
|
106
|
+
const overallDuration = (batchEndTime - metrics.startTime) / 1000;
|
|
107
|
+
const currentRate = (metrics.totalProcessed / overallDuration).toFixed(0);
|
|
108
|
+
|
|
109
|
+
console.log(new Date().toISOString(), `[C${consumerId}] Batch: ${messages.length} msgs | Time: ${(batchDuration/1000).toFixed(3)}s (pop:${(popTime/1000).toFixed(3)}s ack:${(ackTime/1000).toFixed(3)}s) | Total: ${metrics.totalProcessed} | Rate: ${currentRate} msg/s`);
|
|
110
|
+
metrics.lastMessageTime = Date.now();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(new Date().toISOString(), `[Consumer ${consumerId}] Error processing batch:`, error);
|
|
113
|
+
await q.ack(messages, false, { error: error.message });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`[Consumer ${consumerId}] Fatal error:`, error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
consumerMetrics.endTime = Date.now();
|
|
121
|
+
console.log(`[Consumer ${consumerId}] Finished. Processed ${consumerMetrics.messagesProcessed} messages in ${consumerMetrics.batchesProcessed} batches`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Start metrics
|
|
125
|
+
metrics.startTime = Date.now();
|
|
126
|
+
|
|
127
|
+
// Launch all consumers
|
|
128
|
+
const consumerPromises = [];
|
|
129
|
+
for (let i = 0; i < NUMBER_OF_CONSUMERS; i++) {
|
|
130
|
+
consumerPromises.push(consumer(i, i));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Wait for all consumers to complete
|
|
134
|
+
await Promise.all(consumerPromises);
|
|
135
|
+
metrics.endTime = Date.now();
|
|
136
|
+
|
|
137
|
+
// Calculate actual processing time (excluding idle timeout)
|
|
138
|
+
// Find the time when the last batch was actually completed
|
|
139
|
+
let lastBatchCompletedTime = metrics.startTime;
|
|
140
|
+
metrics.consumers.forEach(c => {
|
|
141
|
+
if (c.endTime && c.batchesProcessed > 0) {
|
|
142
|
+
// c.endTime is when consumer exited (after idle timeout)
|
|
143
|
+
// Actual last batch time = c.endTime - (idle timeout we didn't track)
|
|
144
|
+
// Better: use the sum of all processing times
|
|
145
|
+
const totalBatchTime = c.batchTimes.reduce((a,b)=>a+b,0);
|
|
146
|
+
const totalPopTime = c.popTimes.reduce((a,b)=>a+b,0);
|
|
147
|
+
const actualEndTime = c.startTime + totalBatchTime + totalPopTime;
|
|
148
|
+
if (actualEndTime > lastBatchCompletedTime) {
|
|
149
|
+
lastBatchCompletedTime = actualEndTime;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const totalDuration = (metrics.endTime - metrics.startTime) / 1000;
|
|
155
|
+
const actualProcessingDuration = (lastBatchCompletedTime - metrics.startTime) / 1000;
|
|
156
|
+
const avgThroughput = (metrics.totalProcessed / actualProcessingDuration).toFixed(0);
|
|
157
|
+
|
|
158
|
+
console.log('\n' + '='.repeat(80));
|
|
159
|
+
console.log('📊 BENCHMARK RESULTS');
|
|
160
|
+
console.log('='.repeat(80));
|
|
161
|
+
console.log(`Total Messages Processed: ${metrics.totalProcessed.toLocaleString()}`);
|
|
162
|
+
console.log(`Total Wall Clock Time: ${totalDuration.toFixed(2)}s (includes idle timeout)`);
|
|
163
|
+
console.log(`Actual Processing Time: ${actualProcessingDuration.toFixed(2)}s`);
|
|
164
|
+
console.log(`Idle/Wait Time: ${(totalDuration - actualProcessingDuration).toFixed(2)}s`);
|
|
165
|
+
console.log(`Average Throughput: ${avgThroughput} msg/s (based on processing time)`);
|
|
166
|
+
console.log(`Number of Consumers: ${NUMBER_OF_CONSUMERS}`);
|
|
167
|
+
console.log(`Total Batches: ${metrics.batchTimes.length}`);
|
|
168
|
+
console.log(`Total Time: ${metrics.lastMessageTime - metrics.firstMessageTime}ms`);
|
|
169
|
+
console.log(`Total msg/s: ${metrics.totalProcessed / (metrics.lastMessageTime - metrics.firstMessageTime) * 1000} msg/s`);
|
|
170
|
+
|
|
171
|
+
if (metrics.batchTimes.length > 0) {
|
|
172
|
+
const avgBatchTime = (metrics.batchTimes.reduce((a, b) => a + b, 0) / metrics.batchTimes.length / 1000).toFixed(3);
|
|
173
|
+
const minBatchTime = (Math.min(...metrics.batchTimes) / 1000).toFixed(3);
|
|
174
|
+
const maxBatchTime = (Math.max(...metrics.batchTimes) / 1000).toFixed(3);
|
|
175
|
+
|
|
176
|
+
console.log(`\nBatch Times:`);
|
|
177
|
+
console.log(` Average: ${avgBatchTime}s`);
|
|
178
|
+
console.log(` Min: ${minBatchTime}s`);
|
|
179
|
+
console.log(` Max: ${maxBatchTime}s`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (metrics.popTimes.length > 0) {
|
|
183
|
+
const avgPopTime = (metrics.popTimes.reduce((a, b) => a + b, 0) / metrics.popTimes.length / 1000).toFixed(3);
|
|
184
|
+
console.log(`\nAverage POP Time: ${avgPopTime}s`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (metrics.ackTimes.length > 0) {
|
|
188
|
+
const avgAckTime = (metrics.ackTimes.reduce((a, b) => a + b, 0) / metrics.ackTimes.length / 1000).toFixed(3);
|
|
189
|
+
console.log(`Average ACK Time: ${avgAckTime}s`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`\nPer-Consumer Stats:`);
|
|
193
|
+
metrics.consumers.forEach(c => {
|
|
194
|
+
// Calculate actual processing time (sum of all batch times + pop times)
|
|
195
|
+
const totalBatchTime = c.batchTimes.reduce((a,b)=>a+b,0);
|
|
196
|
+
const totalPopTime = c.popTimes.reduce((a,b)=>a+b,0);
|
|
197
|
+
const actualProcessingTime = (totalBatchTime + totalPopTime) / 1000;
|
|
198
|
+
|
|
199
|
+
const wallClockTime = ((c.endTime - c.startTime) / 1000).toFixed(2);
|
|
200
|
+
const rate = (c.messagesProcessed / actualProcessingTime).toFixed(0);
|
|
201
|
+
const avgBatch = c.batchTimes.length > 0 ? (totalBatchTime / c.batchTimes.length / 1000).toFixed(3) : 'N/A';
|
|
202
|
+
const idleTime = (wallClockTime - actualProcessingTime).toFixed(2);
|
|
203
|
+
|
|
204
|
+
console.log(` [C${c.consumerId}] ${c.messagesProcessed.toLocaleString()} msgs | Active: ${actualProcessingTime.toFixed(2)}s (${rate} msg/s) | Idle: ${idleTime}s | ${c.batchesProcessed} batches | avg: ${avgBatch}s`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
console.log('='.repeat(80));
|