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