queen-mq 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +276 -53
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-dashboard-api.js +200 -0
- package/package.json +4 -2
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +192 -16
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/advanced-pattern-tests.js +5 -5
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/utils.js +1 -1
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/src/routes/analytics.js +0 -812
|
@@ -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
|
-
//
|
|
52
|
-
this.
|
|
53
|
-
|
|
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
|
package/src/routes/messages.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
79
|
-
|
|
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.
|
|
126
|
-
|
|
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.
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 (
|
|
177
|
-
throw new Error('Message not found
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
`, [
|
|
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
|
|
254
|
+
throw new Error('Message already in DLQ for this consumer group');
|
|
198
255
|
}
|
|
199
256
|
|
|
200
|
-
log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${
|
|
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.
|
|
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
|
-
//
|
|
17
|
-
|
|
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
|
|
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
|
|
44
|
+
// Wait for message notification or current poll interval
|
|
70
45
|
const notification = await Promise.race([
|
|
71
|
-
eventManager.waitForMessage(queuePath,
|
|
72
|
-
new Promise(resolve => setTimeout(() => resolve(null),
|
|
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.
|
|
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
|
package/src/routes/resources.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
17
|
-
COUNT(DISTINCT CASE WHEN
|
|
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.
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
COUNT(
|
|
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.
|
|
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
|
-
|
|
155
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
261
|
-
|
|
262
|
-
(SELECT COUNT(*) FROM queen.
|
|
263
|
-
|
|
264
|
-
|
|
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
|
};
|