queen-mq 0.1.1 → 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 +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +276 -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/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 +192 -16
- 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/advanced-pattern-tests.js +5 -5
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/utils.js +1 -1
- 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/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/routes/analytics.js +0 -812
|
@@ -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.2",
|
|
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));
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Queen } from '../client/client.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
const NAMESPACE = 'benchmark';
|
|
5
|
+
const NUMBER_OF_CONSUMERS = 10;
|
|
6
|
+
const BATCH_SIZE = 1;
|
|
7
|
+
|
|
8
|
+
// Global metrics tracking
|
|
9
|
+
const metrics = {
|
|
10
|
+
startTime: null,
|
|
11
|
+
endTime: null,
|
|
12
|
+
totalProcessed: 0,
|
|
13
|
+
consumers: [],
|
|
14
|
+
batchTimes: [],
|
|
15
|
+
ackTimes: [],
|
|
16
|
+
popTimes: [],
|
|
17
|
+
firstMessageTime: null,
|
|
18
|
+
lastMessageTime: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function consumer(consumerId) {
|
|
22
|
+
const q = new Queen({
|
|
23
|
+
baseUrls: ['http://localhost:6632'],
|
|
24
|
+
timeout: 30000
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const consumerMetrics = {
|
|
28
|
+
consumerId,
|
|
29
|
+
messagesProcessed: 0,
|
|
30
|
+
batchesProcessed: 0,
|
|
31
|
+
batchTimes: [],
|
|
32
|
+
ackTimes: [],
|
|
33
|
+
popTimes: [],
|
|
34
|
+
startTime: Date.now(),
|
|
35
|
+
endTime: null,
|
|
36
|
+
queuesConsumed: new Set(),
|
|
37
|
+
partitionsConsumed: new Set()
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const target = `namespace:${NAMESPACE}`;
|
|
41
|
+
|
|
42
|
+
metrics.consumers.push(consumerMetrics);
|
|
43
|
+
console.log(`[Consumer ${consumerId}] Started, consuming from namespace '${NAMESPACE}'`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
for await (const messages of q.takeBatch(target, {
|
|
47
|
+
wait: true,
|
|
48
|
+
timeout: 15000,
|
|
49
|
+
batch: BATCH_SIZE,
|
|
50
|
+
idleTimeout: 5000
|
|
51
|
+
})) {
|
|
52
|
+
if (!metrics.firstMessageTime) {
|
|
53
|
+
metrics.firstMessageTime = Date.now();
|
|
54
|
+
}
|
|
55
|
+
metrics.lastMessageTime = Date.now();
|
|
56
|
+
const batchStartTime = Date.now();
|
|
57
|
+
const popTime = batchStartTime - (consumerMetrics.endTime || consumerMetrics.startTime);
|
|
58
|
+
consumerMetrics.popTimes.push(popTime);
|
|
59
|
+
metrics.popTimes.push(popTime);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Track which queues and partitions this consumer is consuming from
|
|
63
|
+
for (const message of messages) {
|
|
64
|
+
if (message.queue) consumerMetrics.queuesConsumed.add(message.queue);
|
|
65
|
+
if (message.partition !== undefined) consumerMetrics.partitionsConsumed.add(`${message.queue}:${message.partition}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate message ordering per queue/partition
|
|
69
|
+
const queuePartitionLastIds = new Map();
|
|
70
|
+
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
if (!message.data || typeof message.data.id !== 'number') {
|
|
73
|
+
throw new Error(`Message missing or invalid data.id: ${JSON.stringify(message)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Use queue and partition to ensure proper ordering within each queue/partition
|
|
77
|
+
const queuePartitionKey = `${message.queue || 'default'}:${message.partition || 'default'}`;
|
|
78
|
+
const messageId = message.data.id;
|
|
79
|
+
|
|
80
|
+
if (queuePartitionLastIds.has(queuePartitionKey)) {
|
|
81
|
+
const lastId = queuePartitionLastIds.get(queuePartitionKey);
|
|
82
|
+
if (messageId !== lastId + 1) {
|
|
83
|
+
console.log(`Queue: ${message.queue}, Partition: ${message.partition}, Message:`, message);
|
|
84
|
+
throw new Error(`Message ordering violation in '${queuePartitionKey}': expected id ${lastId + 1}, got ${messageId}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
queuePartitionLastIds.set(queuePartitionKey, messageId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Acknowledge all messages
|
|
92
|
+
const ackStartTime = Date.now();
|
|
93
|
+
await q.ack(messages);
|
|
94
|
+
const ackTime = Date.now() - ackStartTime;
|
|
95
|
+
consumerMetrics.ackTimes.push(ackTime);
|
|
96
|
+
metrics.ackTimes.push(ackTime);
|
|
97
|
+
|
|
98
|
+
const batchEndTime = Date.now();
|
|
99
|
+
const batchDuration = batchEndTime - batchStartTime;
|
|
100
|
+
|
|
101
|
+
consumerMetrics.messagesProcessed += messages.length;
|
|
102
|
+
consumerMetrics.batchesProcessed++;
|
|
103
|
+
consumerMetrics.batchTimes.push(batchDuration);
|
|
104
|
+
consumerMetrics.endTime = batchEndTime;
|
|
105
|
+
|
|
106
|
+
metrics.totalProcessed += messages.length;
|
|
107
|
+
metrics.batchTimes.push(batchDuration);
|
|
108
|
+
|
|
109
|
+
const overallDuration = (batchEndTime - metrics.startTime) / 1000;
|
|
110
|
+
const currentRate = (metrics.totalProcessed / overallDuration).toFixed(0);
|
|
111
|
+
|
|
112
|
+
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`);
|
|
113
|
+
metrics.lastMessageTime = Date.now();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(new Date().toISOString(), `[Consumer ${consumerId}] Error processing batch:`, error);
|
|
116
|
+
await q.ack(messages, false, { error: error.message });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`[Consumer ${consumerId}] Fatal error:`, error);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
consumerMetrics.endTime = Date.now();
|
|
124
|
+
console.log(`[Consumer ${consumerId}] Finished. Processed ${consumerMetrics.messagesProcessed} messages in ${consumerMetrics.batchesProcessed} batches`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Start metrics
|
|
128
|
+
metrics.startTime = Date.now();
|
|
129
|
+
|
|
130
|
+
// Launch all consumers (they will all consume from the namespace)
|
|
131
|
+
console.log(`Launching ${NUMBER_OF_CONSUMERS} consumers for namespace '${NAMESPACE}'...\n`);
|
|
132
|
+
const consumerPromises = [];
|
|
133
|
+
for (let i = 0; i < NUMBER_OF_CONSUMERS; i++) {
|
|
134
|
+
consumerPromises.push(consumer(i));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Wait for all consumers to complete
|
|
138
|
+
await Promise.all(consumerPromises);
|
|
139
|
+
metrics.endTime = Date.now();
|
|
140
|
+
|
|
141
|
+
// Calculate actual processing time (excluding idle timeout)
|
|
142
|
+
// Find the time when the last batch was actually completed
|
|
143
|
+
let lastBatchCompletedTime = metrics.startTime;
|
|
144
|
+
metrics.consumers.forEach(c => {
|
|
145
|
+
if (c.endTime && c.batchesProcessed > 0) {
|
|
146
|
+
// c.endTime is when consumer exited (after idle timeout)
|
|
147
|
+
// Actual last batch time = c.endTime - (idle timeout we didn't track)
|
|
148
|
+
// Better: use the sum of all processing times
|
|
149
|
+
const totalBatchTime = c.batchTimes.reduce((a,b)=>a+b,0);
|
|
150
|
+
const totalPopTime = c.popTimes.reduce((a,b)=>a+b,0);
|
|
151
|
+
const actualEndTime = c.startTime + totalBatchTime + totalPopTime;
|
|
152
|
+
if (actualEndTime > lastBatchCompletedTime) {
|
|
153
|
+
lastBatchCompletedTime = actualEndTime;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const totalDuration = (metrics.endTime - metrics.startTime) / 1000;
|
|
159
|
+
const actualProcessingDuration = (lastBatchCompletedTime - metrics.startTime) / 1000;
|
|
160
|
+
const avgThroughput = (metrics.totalProcessed / actualProcessingDuration).toFixed(0);
|
|
161
|
+
|
|
162
|
+
console.log('\n' + '='.repeat(80));
|
|
163
|
+
console.log('📊 BENCHMARK RESULTS');
|
|
164
|
+
console.log('='.repeat(80));
|
|
165
|
+
console.log(`Total Messages Processed: ${metrics.totalProcessed.toLocaleString()}`);
|
|
166
|
+
console.log(`Total Wall Clock Time: ${totalDuration.toFixed(2)}s (includes idle timeout)`);
|
|
167
|
+
console.log(`Actual Processing Time: ${actualProcessingDuration.toFixed(2)}s`);
|
|
168
|
+
console.log(`Idle/Wait Time: ${(totalDuration - actualProcessingDuration).toFixed(2)}s`);
|
|
169
|
+
console.log(`Average Throughput: ${avgThroughput} msg/s (based on processing time)`);
|
|
170
|
+
console.log(`Number of Consumers: ${NUMBER_OF_CONSUMERS}`);
|
|
171
|
+
console.log(`Total Batches: ${metrics.batchTimes.length}`);
|
|
172
|
+
console.log(`Total Time: ${metrics.lastMessageTime - metrics.firstMessageTime}ms`);
|
|
173
|
+
console.log(`Total msg/s: ${metrics.totalProcessed / (metrics.lastMessageTime - metrics.firstMessageTime) * 1000} msg/s`);
|
|
174
|
+
|
|
175
|
+
if (metrics.batchTimes.length > 0) {
|
|
176
|
+
const avgBatchTime = (metrics.batchTimes.reduce((a, b) => a + b, 0) / metrics.batchTimes.length / 1000).toFixed(3);
|
|
177
|
+
const minBatchTime = (Math.min(...metrics.batchTimes) / 1000).toFixed(3);
|
|
178
|
+
const maxBatchTime = (Math.max(...metrics.batchTimes) / 1000).toFixed(3);
|
|
179
|
+
|
|
180
|
+
console.log(`\nBatch Times:`);
|
|
181
|
+
console.log(` Average: ${avgBatchTime}s`);
|
|
182
|
+
console.log(` Min: ${minBatchTime}s`);
|
|
183
|
+
console.log(` Max: ${maxBatchTime}s`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (metrics.popTimes.length > 0) {
|
|
187
|
+
const avgPopTime = (metrics.popTimes.reduce((a, b) => a + b, 0) / metrics.popTimes.length / 1000).toFixed(3);
|
|
188
|
+
console.log(`\nAverage POP Time: ${avgPopTime}s`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (metrics.ackTimes.length > 0) {
|
|
192
|
+
const avgAckTime = (metrics.ackTimes.reduce((a, b) => a + b, 0) / metrics.ackTimes.length / 1000).toFixed(3);
|
|
193
|
+
console.log(`Average ACK Time: ${avgAckTime}s`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`\nPer-Consumer Stats:`);
|
|
197
|
+
metrics.consumers.forEach(c => {
|
|
198
|
+
// Calculate actual processing time (sum of all batch times + pop times)
|
|
199
|
+
const totalBatchTime = c.batchTimes.reduce((a,b)=>a+b,0);
|
|
200
|
+
const totalPopTime = c.popTimes.reduce((a,b)=>a+b,0);
|
|
201
|
+
const actualProcessingTime = (totalBatchTime + totalPopTime) / 1000;
|
|
202
|
+
|
|
203
|
+
const wallClockTime = ((c.endTime - c.startTime) / 1000).toFixed(2);
|
|
204
|
+
const rate = (c.messagesProcessed / actualProcessingTime).toFixed(0);
|
|
205
|
+
const avgBatch = c.batchTimes.length > 0 ? (totalBatchTime / c.batchTimes.length / 1000).toFixed(3) : 'N/A';
|
|
206
|
+
const idleTime = (wallClockTime - actualProcessingTime).toFixed(2);
|
|
207
|
+
|
|
208
|
+
const queuesStr = c.queuesConsumed.size > 0 ? `queues:[${Array.from(c.queuesConsumed).sort().join(',')}]` : '';
|
|
209
|
+
const partitionsStr = c.partitionsConsumed.size > 0 ? `partitions:${c.partitionsConsumed.size}` : '';
|
|
210
|
+
const consumedFrom = [queuesStr, partitionsStr].filter(Boolean).join(' ');
|
|
211
|
+
|
|
212
|
+
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 | ${consumedFrom}`);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log('='.repeat(80));
|
|
216
|
+
|