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.
Files changed (139) hide show
  1. package/API.md +1226 -0
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +276 -53
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-dashboard-api.js +200 -0
  17. package/package.json +4 -2
  18. package/server.log +1 -0
  19. package/src/benchmark/consumer.js +207 -0
  20. package/src/benchmark/consumer_multi.js +216 -0
  21. package/src/benchmark/producer.js +75 -0
  22. package/src/benchmark/producer_multi.js +115 -0
  23. package/src/client/client.js +192 -16
  24. package/src/client/queenClient.js +5 -0
  25. package/src/cluster-server.js +242 -0
  26. package/src/config.js +19 -5
  27. package/src/database/connection.js +42 -16
  28. package/src/database/poolManager.js +7 -0
  29. package/src/database/schema-v2.sql +194 -130
  30. package/src/managers/queueManagerOptimized.js +823 -933
  31. package/src/managers/systemEventManager.js +8 -3
  32. package/src/routes/messages.js +127 -57
  33. package/src/routes/pop.js +27 -43
  34. package/src/routes/resources.js +61 -27
  35. package/src/routes/status.js +1037 -0
  36. package/src/server.js +308 -272
  37. package/src/services/evictionService.js +57 -28
  38. package/src/services/retentionService.js +44 -11
  39. package/src/test/advanced-pattern-tests.js +5 -5
  40. package/src/test/bus-mode-tests.js +24 -11
  41. package/src/test/edge-case-tests.js +12 -5
  42. package/src/test/enterprise-tests.js +48 -15
  43. package/src/test/utils.js +1 -1
  44. package/src/utils/streaming.js +231 -0
  45. package/src/utils/uuid.js +2 -2
  46. package/src/websocket/wsServer.js +10 -3
  47. package/test-keepalive-v2.sh +22 -0
  48. package/webapp/COLOR_GUIDE.md +118 -0
  49. package/webapp/README.md +143 -0
  50. package/webapp/index.html +14 -0
  51. package/webapp/package-lock.json +3184 -0
  52. package/webapp/package.json +25 -0
  53. package/webapp/postcss.config.js +7 -0
  54. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  55. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  56. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  57. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  58. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  59. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  60. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  61. package/webapp/public/assets/queen-logo.svg +263 -0
  62. package/webapp/src/App.vue +19 -0
  63. package/webapp/src/api/analytics.js +10 -0
  64. package/webapp/src/api/client.js +29 -0
  65. package/webapp/src/api/consumers.js +52 -0
  66. package/webapp/src/api/health.js +7 -0
  67. package/webapp/src/api/messages.js +26 -0
  68. package/webapp/src/api/queues.js +14 -0
  69. package/webapp/src/api/resources.js +8 -0
  70. package/webapp/src/assets/styles/main.css +357 -0
  71. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  72. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  73. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  74. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  75. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  76. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  77. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  78. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  79. package/webapp/src/components/common/MetricCard.vue +43 -0
  80. package/webapp/src/components/common/StatusBadge.vue +45 -0
  81. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  82. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  83. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  84. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  85. package/webapp/src/components/layout/AppLayout.vue +110 -0
  86. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  87. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  88. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  89. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  90. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  91. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  92. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  93. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  94. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  95. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  96. package/webapp/src/composables/useApi.js +34 -0
  97. package/webapp/src/composables/useTheme.js +36 -0
  98. package/webapp/src/main.js +11 -0
  99. package/webapp/src/router/index.js +42 -0
  100. package/webapp/src/utils/colors.js +96 -0
  101. package/webapp/src/utils/formatters.js +49 -0
  102. package/webapp/src/views/Analytics.vue +377 -0
  103. package/webapp/src/views/ConsumerGroups.vue +433 -0
  104. package/webapp/src/views/Dashboard.vue +418 -0
  105. package/webapp/src/views/Messages.vue +361 -0
  106. package/webapp/src/views/QueueDetail.vue +582 -0
  107. package/webapp/src/views/Queues.vue +496 -0
  108. package/webapp/tailwind.config.js +25 -0
  109. package/webapp/vite.config.js +10 -0
  110. package/dashboard/.vscode/extensions.json +0 -3
  111. package/dashboard/README.md +0 -5
  112. package/dashboard/index.html +0 -14
  113. package/dashboard/package-lock.json +0 -1458
  114. package/dashboard/package.json +0 -25
  115. package/dashboard/public/vite.svg +0 -1
  116. package/dashboard/src/App.vue +0 -29
  117. package/dashboard/src/assets/styles/main.css +0 -908
  118. package/dashboard/src/assets/vue.svg +0 -1
  119. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  120. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  121. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  122. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  123. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  124. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  125. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  126. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  127. package/dashboard/src/main.js +0 -44
  128. package/dashboard/src/router.js +0 -54
  129. package/dashboard/src/services/api.js +0 -187
  130. package/dashboard/src/services/websocket.js +0 -167
  131. package/dashboard/src/utils/constants.js +0 -56
  132. package/dashboard/src/utils/helpers.js +0 -118
  133. package/dashboard/src/views/Analytics.vue +0 -912
  134. package/dashboard/src/views/Dashboard.vue +0 -906
  135. package/dashboard/src/views/Messages.vue +0 -437
  136. package/dashboard/src/views/QueueDetail.vue +0 -501
  137. package/dashboard/src/views/Queues.vue +0 -333
  138. package/dashboard/vite.config.js +0 -30
  139. 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.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
+