queen-mq 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  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-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. package/src/routes/analytics.js +0 -812
@@ -1,4 +1,5 @@
1
1
  import { generateUUID } from '../utils/uuid.js';
2
+ import config from '../config.js';
2
3
 
3
4
  export const SYSTEM_QUEUE = '__system_events__';
4
5
 
@@ -24,6 +25,7 @@ export class SystemEventManager {
24
25
  this.handlers = new Map();
25
26
  this.eventQueue = [];
26
27
  this.batchTimer = null;
28
+ this.enabled = config.SYSTEM_EVENTS.ENABLED;
27
29
  }
28
30
 
29
31
  // Register event handlers
@@ -48,9 +50,12 @@ export class SystemEventManager {
48
50
  // Handle locally first (immediate consistency for originating server)
49
51
  await this.handleEventLocally(event);
50
52
 
51
- // Queue for batch publishing
52
- this.eventQueue.push(event);
53
- this.scheduleBatch();
53
+ // Only publish to system queue if system events are enabled
54
+ if (this.enabled) {
55
+ // Queue for batch publishing
56
+ this.eventQueue.push(event);
57
+ this.scheduleBatch();
58
+ }
54
59
  }
55
60
 
56
61
  // Process incoming event from system queue
@@ -12,23 +12,31 @@ export const createMessagesRoutes = (pool, queueManager) => {
12
12
  m.id,
13
13
  m.transaction_id,
14
14
  m.payload,
15
- m.status,
16
- m.worker_id,
17
15
  m.created_at,
18
- m.locked_at,
19
- m.completed_at,
20
- m.failed_at,
21
- m.error_message,
22
- m.retry_count,
23
- m.lease_expires_at,
16
+ m.trace_id,
24
17
  q.name || '/' || p.name as queue_path,
25
18
  q.name as queue_name,
26
19
  p.name as partition_name,
27
20
  q.namespace,
28
- q.task
21
+ q.task,
22
+ q.priority as queue_priority,
23
+ pc.lease_expires_at,
24
+ pc.last_consumed_created_at,
25
+ pc.last_consumed_id,
26
+ CASE
27
+ WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
28
+ WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
29
+ WHEN pc.last_consumed_created_at IS NOT NULL AND (
30
+ m.created_at < pc.last_consumed_created_at OR
31
+ (m.created_at = pc.last_consumed_created_at AND m.id <= pc.last_consumed_id)
32
+ ) THEN 'completed'
33
+ ELSE 'pending'
34
+ END as message_status
29
35
  FROM queen.messages m
30
36
  JOIN queen.partitions p ON p.id = m.partition_id
31
37
  JOIN queen.queues q ON q.id = p.queue_id
38
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
39
+ LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
32
40
  WHERE 1=1
33
41
  `;
34
42
 
@@ -52,11 +60,20 @@ export const createMessagesRoutes = (pool, queueManager) => {
52
60
  query += ` AND q.task = $${++paramCount}`;
53
61
  }
54
62
  if (status) {
63
+ // Filter by computed status
55
64
  params.push(status);
56
- query += ` AND m.status = $${++paramCount}`;
65
+ query += ` AND CASE
66
+ WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
67
+ WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
68
+ WHEN pc.last_consumed_created_at IS NOT NULL AND (
69
+ m.created_at < pc.last_consumed_created_at OR
70
+ (m.created_at = pc.last_consumed_created_at AND m.id <= pc.last_consumed_id)
71
+ ) THEN 'completed'
72
+ ELSE 'pending'
73
+ END = $${++paramCount}`;
57
74
  }
58
75
 
59
- query += ` ORDER BY m.created_at DESC`;
76
+ query += ` ORDER BY m.created_at DESC, m.id DESC`;
60
77
 
61
78
  params.push(limit);
62
79
  query += ` LIMIT $${++paramCount}`;
@@ -75,14 +92,10 @@ export const createMessagesRoutes = (pool, queueManager) => {
75
92
  namespace: row.namespace,
76
93
  task: row.task,
77
94
  payload: row.payload,
78
- status: row.status,
79
- workerId: row.worker_id,
95
+ status: row.message_status,
96
+ traceId: row.trace_id,
97
+ queuePriority: row.queue_priority,
80
98
  createdAt: row.created_at,
81
- lockedAt: row.locked_at,
82
- completedAt: row.completed_at,
83
- failedAt: row.failed_at,
84
- errorMessage: row.error_message,
85
- retryCount: row.retry_count,
86
99
  leaseExpiresAt: row.lease_expires_at
87
100
  }));
88
101
  };
@@ -91,7 +104,12 @@ export const createMessagesRoutes = (pool, queueManager) => {
91
104
  const getMessage = async (transactionId) => {
92
105
  const result = await pool.query(`
93
106
  SELECT
94
- m.*,
107
+ m.id,
108
+ m.transaction_id,
109
+ m.partition_id,
110
+ m.payload,
111
+ m.created_at,
112
+ m.trace_id,
95
113
  q.name || '/' || p.name as queue_path,
96
114
  q.name as queue_name,
97
115
  p.name as partition_name,
@@ -101,10 +119,26 @@ export const createMessagesRoutes = (pool, queueManager) => {
101
119
  q.retry_limit,
102
120
  q.retry_delay,
103
121
  q.ttl,
104
- q.priority
122
+ q.priority as queue_priority,
123
+ pc.lease_expires_at,
124
+ pc.last_consumed_created_at,
125
+ pc.last_consumed_id,
126
+ dlq.error_message,
127
+ dlq.retry_count,
128
+ CASE
129
+ WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
130
+ WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
131
+ WHEN pc.last_consumed_created_at IS NOT NULL AND (
132
+ m.created_at < pc.last_consumed_created_at OR
133
+ (m.created_at = pc.last_consumed_created_at AND m.id <= pc.last_consumed_id)
134
+ ) THEN 'completed'
135
+ ELSE 'pending'
136
+ END as message_status
105
137
  FROM queen.messages m
106
138
  JOIN queen.partitions p ON p.id = m.partition_id
107
139
  JOIN queen.queues q ON q.id = p.queue_id
140
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
141
+ LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
108
142
  WHERE m.transaction_id = $1
109
143
  `, [transactionId]);
110
144
 
@@ -122,12 +156,9 @@ export const createMessagesRoutes = (pool, queueManager) => {
122
156
  namespace: row.namespace,
123
157
  task: row.task,
124
158
  payload: row.payload,
125
- status: row.status,
126
- workerId: row.worker_id,
159
+ status: row.message_status,
160
+ traceId: row.trace_id,
127
161
  createdAt: row.created_at,
128
- lockedAt: row.locked_at,
129
- completedAt: row.completed_at,
130
- failedAt: row.failed_at,
131
162
  errorMessage: row.error_message,
132
163
  retryCount: row.retry_count,
133
164
  leaseExpiresAt: row.lease_expires_at,
@@ -136,7 +167,7 @@ export const createMessagesRoutes = (pool, queueManager) => {
136
167
  retryLimit: row.retry_limit,
137
168
  retryDelay: row.retry_delay,
138
169
  ttl: row.ttl,
139
- priority: row.priority
170
+ priority: row.queue_priority
140
171
  }
141
172
  };
142
173
  };
@@ -157,48 +188,74 @@ export const createMessagesRoutes = (pool, queueManager) => {
157
188
  return { deleted: true, transactionId };
158
189
  };
159
190
 
160
- // Retry message
191
+ // Retry message (remove from DLQ - cursor will process it if not consumed yet)
161
192
  const retryMessage = async (transactionId) => {
162
- const result = await pool.query(`
163
- UPDATE queen.messages
164
- SET
165
- status = 'pending',
166
- worker_id = NULL,
167
- locked_at = NULL,
168
- lease_expires_at = NULL,
169
- retry_count = retry_count + 1,
170
- error_message = NULL
171
- WHERE transaction_id = $1
172
- AND status IN ('failed', 'dead_letter')
173
- RETURNING id, retry_count
193
+ // Find the message
194
+ const msgCheck = await pool.query(`
195
+ SELECT m.id, m.partition_id, m.created_at,
196
+ dlq.consumer_group, dlq.retry_count
197
+ FROM queen.messages m
198
+ LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
199
+ WHERE m.transaction_id = $1
174
200
  `, [transactionId]);
175
201
 
176
- if (result.rows.length === 0) {
177
- throw new Error('Message not found or not in failed state');
202
+ if (msgCheck.rows.length === 0) {
203
+ throw new Error('Message not found');
204
+ }
205
+
206
+ const msg = msgCheck.rows[0];
207
+
208
+ if (!msg.consumer_group) {
209
+ throw new Error('Message is not in DLQ, cannot retry');
178
210
  }
179
211
 
180
- log(`${LogTypes.RETRY} | TransactionId: ${transactionId} | NewRetryCount: ${result.rows[0].retry_count}`);
181
- return { retried: true, transactionId };
212
+ const consumerGroup = msg.consumer_group;
213
+ const newRetryCount = (msg.retry_count || 0) + 1;
214
+
215
+ // Remove from DLQ - message will be available for consumption if before cursor
216
+ const deleteResult = await pool.query(`
217
+ DELETE FROM queen.dead_letter_queue
218
+ WHERE message_id = $1 AND consumer_group = $2
219
+ RETURNING message_id
220
+ `, [msg.id, consumerGroup]);
221
+
222
+ if (deleteResult.rows.length === 0) {
223
+ throw new Error('Message not found in DLQ');
224
+ }
225
+
226
+ log(`${LogTypes.RETRY} | TransactionId: ${transactionId} | ConsumerGroup: ${consumerGroup}`);
227
+ return { retried: true, transactionId, consumerGroup };
182
228
  };
183
229
 
184
230
  // Move message to DLQ
185
- const moveToDLQ = async (transactionId) => {
231
+ const moveToDLQ = async (transactionId, consumerGroup = '__QUEUE_MODE__', errorMessage = 'Manually moved to DLQ') => {
232
+ // Find the message
233
+ const msgCheck = await pool.query(`
234
+ SELECT m.id, m.partition_id, m.created_at
235
+ FROM queen.messages m
236
+ WHERE m.transaction_id = $1
237
+ `, [transactionId]);
238
+
239
+ if (msgCheck.rows.length === 0) {
240
+ throw new Error('Message not found');
241
+ }
242
+
243
+ const msg = msgCheck.rows[0];
244
+
245
+ // Insert into dead_letter_queue
186
246
  const result = await pool.query(`
187
- UPDATE queen.messages
188
- SET
189
- status = 'dead_letter',
190
- failed_at = NOW()
191
- WHERE transaction_id = $1
192
- AND status = 'failed'
247
+ INSERT INTO queen.dead_letter_queue (message_id, partition_id, consumer_group, error_message, retry_count, original_created_at)
248
+ VALUES ($1, $2, $3, $4, 0, $5)
249
+ ON CONFLICT DO NOTHING
193
250
  RETURNING id
194
- `, [transactionId]);
251
+ `, [msg.id, msg.partition_id, consumerGroup, errorMessage, msg.created_at]);
195
252
 
196
253
  if (result.rows.length === 0) {
197
- throw new Error('Message not found or not in failed state');
254
+ throw new Error('Message already in DLQ for this consumer group');
198
255
  }
199
256
 
200
- log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
201
- return { movedToDLQ: true, transactionId };
257
+ log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${msg.id} | ConsumerGroup: ${consumerGroup}`);
258
+ return { movedToDLQ: true, transactionId, messageId: msg.id };
202
259
  };
203
260
 
204
261
  // Get related messages (same partition, near in time)
@@ -220,10 +277,23 @@ export const createMessagesRoutes = (pool, queueManager) => {
220
277
  const result = await pool.query(`
221
278
  SELECT
222
279
  m.transaction_id,
223
- m.status,
224
280
  m.created_at,
225
- m.payload
281
+ m.payload,
282
+ pc.lease_expires_at,
283
+ pc.last_consumed_created_at,
284
+ pc.last_consumed_id,
285
+ CASE
286
+ WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
287
+ WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
288
+ WHEN pc.last_consumed_created_at IS NOT NULL AND (
289
+ m.created_at < pc.last_consumed_created_at OR
290
+ (m.created_at = pc.last_consumed_created_at AND m.id <= pc.last_consumed_id)
291
+ ) THEN 'completed'
292
+ ELSE 'pending'
293
+ END as message_status
226
294
  FROM queen.messages m
295
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id AND pc.consumer_group = '__QUEUE_MODE__'
296
+ LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
227
297
  WHERE m.partition_id = $1
228
298
  AND m.transaction_id != $2
229
299
  AND m.created_at BETWEEN $3::timestamp - INTERVAL '1 hour'
@@ -234,7 +304,7 @@ export const createMessagesRoutes = (pool, queueManager) => {
234
304
 
235
305
  return result.rows.map(row => ({
236
306
  transactionId: row.transaction_id,
237
- status: row.status,
307
+ status: row.message_status,
238
308
  createdAt: row.created_at,
239
309
  payload: row.payload
240
310
  }));
package/src/routes/pop.js CHANGED
@@ -13,70 +13,54 @@ export const createPopRoute = (queueManager, eventManager) => {
13
13
  const maxTimeout = config.QUEUE.MAX_TIMEOUT;
14
14
  const effectiveTimeout = Math.min(timeout, maxTimeout);
15
15
 
16
- // Handle different pop scenarios
17
- if (scope.namespace || scope.task) {
18
- // Pop with filters (namespace or task)
19
- let result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false, timeout: effectiveTimeout });
20
-
21
- if (result.messages.length > 0 || !wait) {
22
- return result;
23
- }
24
-
25
- // Simple polling for filtered pops (no specific queue path to wait on)
26
- const startTime = Date.now();
27
- const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL_FILTERED, effectiveTimeout / 10);
28
-
29
- while (Date.now() - startTime < effectiveTimeout) {
30
- await new Promise(resolve => setTimeout(resolve, pollInterval));
31
- result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false });
32
- if (result.messages.length > 0) {
33
- return result;
34
- }
35
- }
36
-
37
- return { messages: [] };
38
- }
39
-
40
- // Regular queue/partition pop (now with consumer group support)
41
- const popScope = {
42
- ...scope,
43
- consumerGroup: consumerGroup || scope.consumerGroup
44
- };
45
-
46
- const popOptions = {
16
+ // ALWAYS USE UNIFIED POP - handles all 3 modes (direct, queue-level, filtered)
17
+ let result = await queueManager.uniquePop(scope, {
47
18
  batch,
48
19
  wait: false,
49
- timeout: effectiveTimeout,
50
- subscriptionMode,
51
- subscriptionFrom
52
- };
53
-
54
- let result = await queueManager.popMessages(popScope, popOptions);
20
+ timeout: effectiveTimeout,
21
+ subscriptionMode,
22
+ subscriptionFrom
23
+ });
55
24
 
56
25
  if (result.messages.length > 0 || !wait) {
57
26
  return result;
58
27
  }
59
28
 
60
- // Long polling with event-driven optimization
29
+ // Long polling with event-driven optimization and exponential backoff
61
30
  const queuePath = scope.partition
62
31
  ? `${scope.queue}/${scope.partition}`
63
32
  : scope.queue;
64
33
 
65
34
  const startTime = Date.now();
66
- const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL, effectiveTimeout / 10);
35
+ const initialPollInterval = Math.min(config.QUEUE.POLL_INTERVAL, effectiveTimeout / 10);
36
+ const maxPollInterval = config.QUEUE.MAX_POLL_INTERVAL || 2000; // Default 2 seconds
37
+ const backoffThreshold = config.QUEUE.BACKOFF_THRESHOLD || 5; // Retries before backoff
38
+ const backoffMultiplier = config.QUEUE.BACKOFF_MULTIPLIER || 2;
39
+
40
+ let currentPollInterval = initialPollInterval;
41
+ let consecutiveEmptyPolls = 0;
67
42
 
68
43
  while (Date.now() - startTime < effectiveTimeout) {
69
- // Wait for message notification or short timeout
44
+ // Wait for message notification or current poll interval
70
45
  const notification = await Promise.race([
71
- eventManager.waitForMessage(queuePath, pollInterval),
72
- new Promise(resolve => setTimeout(() => resolve(null), pollInterval))
46
+ eventManager.waitForMessage(queuePath, currentPollInterval),
47
+ new Promise(resolve => setTimeout(() => resolve(null), currentPollInterval))
73
48
  ]);
74
49
 
75
50
  // Try to get messages whether notified or not
76
- result = await queueManager.popMessages(popScope, popOptions);
51
+ result = await queueManager.uniquePop(scope, { batch, wait: false, subscriptionMode, subscriptionFrom });
77
52
  if (result.messages.length > 0) {
78
53
  return result;
79
54
  }
55
+
56
+ // Implement exponential backoff after threshold
57
+ consecutiveEmptyPolls++;
58
+ if (consecutiveEmptyPolls >= backoffThreshold) {
59
+ currentPollInterval = Math.min(
60
+ currentPollInterval * backoffMultiplier,
61
+ maxPollInterval
62
+ );
63
+ }
80
64
  }
81
65
 
82
66
  // Timeout reached, no messages
@@ -1,4 +1,6 @@
1
- export const createResourcesRoutes = (pool) => {
1
+ import { EventTypes } from '../managers/systemEventManager.js';
2
+
3
+ export const createResourcesRoutes = (pool, systemEventManager) => {
2
4
 
3
5
  // Get all queues with their partitions
4
6
  const getQueues = async (filters = {}) => {
@@ -13,12 +15,12 @@ export const createResourcesRoutes = (pool) => {
13
15
  q.created_at as queue_created_at,
14
16
  COUNT(DISTINCT p.id) as partition_count,
15
17
  COUNT(DISTINCT m.id) as message_count,
16
- COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) as pending_count,
17
- COUNT(DISTINCT CASE WHEN ms.status = 'processing' THEN m.id END) as processing_count
18
+ COALESCE(SUM(DISTINCT pc.pending_estimate), 0) as pending_count,
19
+ COUNT(DISTINCT CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN p.id END) as processing_count
18
20
  FROM queen.queues q
19
21
  LEFT JOIN queen.partitions p ON p.queue_id = q.id
20
22
  LEFT JOIN queen.messages m ON m.partition_id = p.id
21
- LEFT JOIN queen.messages_status ms ON ms.message_id = m.id AND ms.consumer_group IS NULL
23
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
22
24
  WHERE 1=1
23
25
  `;
24
26
 
@@ -72,25 +74,27 @@ export const createResourcesRoutes = (pool) => {
72
74
 
73
75
  const queue = queueResult.rows[0];
74
76
 
75
- // Get partitions with stats (partitions no longer have options or priority)
77
+ // Get partitions with stats using partition_consumers
76
78
  const partitionsResult = await pool.query(`
77
79
  SELECT
78
80
  p.id,
79
81
  p.name,
80
82
  p.created_at,
81
83
  COUNT(DISTINCT m.id) as message_count,
82
- COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) as pending,
83
- COUNT(DISTINCT CASE WHEN ms.status = 'processing' THEN m.id END) as processing,
84
- COUNT(DISTINCT CASE WHEN ms.status = 'completed' THEN m.id END) as completed,
85
- COUNT(DISTINCT CASE WHEN ms.status = 'failed' THEN m.id END) as failed,
86
- COUNT(DISTINCT CASE WHEN ms.status = 'dead_letter' THEN m.id END) as dead_letter,
84
+ COALESCE(pc.pending_estimate, 0) as pending,
85
+ CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 1 ELSE 0 END as processing,
86
+ COALESCE(pc.total_messages_consumed, 0) as completed,
87
+ 0 as failed,
88
+ (SELECT COUNT(*) FROM queen.dead_letter_queue dlq
89
+ WHERE dlq.partition_id = p.id
90
+ AND (dlq.consumer_group IS NULL OR dlq.consumer_group = '__QUEUE_MODE__')) as dead_letter,
87
91
  MIN(m.created_at) as oldest_message,
88
92
  MAX(m.created_at) as newest_message
89
93
  FROM queen.partitions p
90
94
  LEFT JOIN queen.messages m ON m.partition_id = p.id
91
- LEFT JOIN queen.messages_status ms ON ms.message_id = m.id AND ms.consumer_group IS NULL
95
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
92
96
  WHERE p.queue_id = $1
93
- GROUP BY p.id, p.name, p.created_at
97
+ GROUP BY p.id, p.name, p.created_at, pc.pending_estimate, pc.lease_expires_at, pc.total_messages_consumed
94
98
  ORDER BY p.name
95
99
  `, [queue.id]);
96
100
 
@@ -151,13 +155,13 @@ export const createResourcesRoutes = (pool) => {
151
155
  q.namespace,
152
156
  q.task,
153
157
  q.priority,
154
- COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) as pending,
155
- COUNT(DISTINCT CASE WHEN ms.status = 'processing' THEN m.id END) as processing,
158
+ COALESCE(pc.pending_estimate, 0) as pending,
159
+ CASE WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 1 ELSE 0 END as processing,
156
160
  COUNT(DISTINCT m.id) as total
157
161
  FROM queen.partitions p
158
162
  JOIN queen.queues q ON q.id = p.queue_id
159
163
  LEFT JOIN queen.messages m ON m.partition_id = p.id
160
- LEFT JOIN queen.messages_status ms ON ms.message_id = m.id AND ms.consumer_group IS NULL
164
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
161
165
  WHERE 1=1
162
166
  `;
163
167
 
@@ -168,11 +172,11 @@ export const createResourcesRoutes = (pool) => {
168
172
  }
169
173
 
170
174
  query += ` GROUP BY p.id, p.name, p.created_at,
171
- q.name, q.namespace, q.task, q.priority`;
175
+ q.name, q.namespace, q.task, q.priority, pc.pending_estimate, pc.lease_expires_at`;
172
176
 
173
177
  if (minDepth) {
174
178
  params.push(minDepth);
175
- query += ` HAVING COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) >= $${params.length}`;
179
+ query += ` HAVING COALESCE(pc.pending_estimate, 0) >= $${params.length}`;
176
180
  }
177
181
 
178
182
  query += ` ORDER BY q.name, q.priority DESC, p.name`;
@@ -201,11 +205,11 @@ export const createResourcesRoutes = (pool) => {
201
205
  COUNT(DISTINCT q.id) as queue_count,
202
206
  COUNT(DISTINCT p.id) as partition_count,
203
207
  COUNT(DISTINCT m.id) as message_count,
204
- COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) as pending_count
208
+ COALESCE(SUM(pc.pending_estimate), 0) as pending_count
205
209
  FROM queen.queues q
206
210
  LEFT JOIN queen.partitions p ON p.queue_id = q.id
207
211
  LEFT JOIN queen.messages m ON m.partition_id = p.id
208
- LEFT JOIN queen.messages_status ms ON ms.message_id = m.id AND ms.consumer_group IS NULL
212
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
209
213
  WHERE q.namespace IS NOT NULL
210
214
  GROUP BY q.namespace
211
215
  ORDER BY q.namespace
@@ -229,11 +233,11 @@ export const createResourcesRoutes = (pool) => {
229
233
  COUNT(DISTINCT q.id) as queue_count,
230
234
  COUNT(DISTINCT p.id) as partition_count,
231
235
  COUNT(DISTINCT m.id) as message_count,
232
- COUNT(DISTINCT CASE WHEN ms.status = 'pending' THEN m.id END) as pending_count
236
+ COALESCE(SUM(pc.pending_estimate), 0) as pending_count
233
237
  FROM queen.queues q
234
238
  LEFT JOIN queen.partitions p ON p.queue_id = q.id
235
239
  LEFT JOIN queen.messages m ON m.partition_id = p.id
236
- LEFT JOIN queen.messages_status ms ON ms.message_id = m.id AND ms.consumer_group IS NULL
240
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
237
241
  WHERE q.task IS NOT NULL
238
242
  GROUP BY q.task
239
243
  ORDER BY q.task
@@ -257,11 +261,16 @@ export const createResourcesRoutes = (pool) => {
257
261
  (SELECT COUNT(*) FROM queen.queues) as total_queues,
258
262
  (SELECT COUNT(*) FROM queen.partitions) as total_partitions,
259
263
  (SELECT COUNT(*) FROM queen.messages) as total_messages,
260
- (SELECT COUNT(*) FROM queen.messages_status WHERE status = 'pending' AND consumer_group IS NULL) as pending_messages,
261
- (SELECT COUNT(*) FROM queen.messages_status WHERE status = 'processing' AND consumer_group IS NULL) as processing_messages,
262
- (SELECT COUNT(*) FROM queen.messages_status WHERE status = 'completed' AND consumer_group IS NULL) as completed_messages,
263
- (SELECT COUNT(*) FROM queen.messages_status WHERE status = 'failed' AND consumer_group IS NULL) as failed_messages,
264
- (SELECT COUNT(*) FROM queen.messages_status WHERE status = 'dead_letter' AND consumer_group IS NULL) as dead_letter_messages,
264
+ (SELECT COALESCE(SUM(pending_estimate), 0)::integer FROM queen.partition_consumers
265
+ WHERE consumer_group = '__QUEUE_MODE__') as pending_messages,
266
+ (SELECT COUNT(*) FROM queen.partition_consumers
267
+ WHERE consumer_group = '__QUEUE_MODE__'
268
+ AND lease_expires_at IS NOT NULL
269
+ AND lease_expires_at > NOW()) as processing_messages,
270
+ (SELECT COALESCE(SUM(total_messages_consumed), 0)::integer FROM queen.partition_consumers
271
+ WHERE consumer_group = '__QUEUE_MODE__') as completed_messages,
272
+ 0 as failed_messages,
273
+ (SELECT COUNT(*) FROM queen.dead_letter_queue WHERE consumer_group IS NULL OR consumer_group = '__QUEUE_MODE__') as dead_letter_messages,
265
274
  (SELECT COUNT(DISTINCT namespace) FROM queen.queues WHERE namespace IS NOT NULL) as namespaces,
266
275
  (SELECT COUNT(DISTINCT task) FROM queen.queues WHERE task IS NOT NULL) as tasks
267
276
  `);
@@ -285,12 +294,37 @@ export const createResourcesRoutes = (pool) => {
285
294
  };
286
295
  };
287
296
 
297
+ // Delete a queue (cascades to partitions, messages, etc.)
298
+ // Idempotent: returns success even if queue doesn't exist
299
+ const deleteQueue = async (queueName) => {
300
+ const result = await pool.query(
301
+ `DELETE FROM queen.queues WHERE name = $1 RETURNING id, name`,
302
+ [queueName]
303
+ );
304
+
305
+ // Emit system event for cache invalidation across nodes
306
+ if (result.rows.length > 0 && systemEventManager) {
307
+ await systemEventManager.emit(EventTypes.QUEUE_DELETED, {
308
+ entityId: queueName,
309
+ queueId: result.rows[0].id
310
+ });
311
+ }
312
+
313
+ return {
314
+ deleted: true,
315
+ queue: queueName,
316
+ existed: result.rows.length > 0,
317
+ id: result.rows.length > 0 ? result.rows[0].id : null
318
+ };
319
+ };
320
+
288
321
  return {
289
322
  getQueues,
290
323
  getQueue,
291
324
  getPartitions,
292
325
  getNamespaces,
293
326
  getTasks,
294
- getSystemOverview
327
+ getSystemOverview,
328
+ deleteQueue
295
329
  };
296
330
  };