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.
- package/API.md +862 -752
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +1705 -1051
- 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-complete-client.js +260 -0
- package/examples/test-dashboard-api.js +200 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +17 -4
- 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 +300 -31
- 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/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +361 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +561 -0
- package/src/test/enterprise-tests.js +637 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- 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/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- 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/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
- package/src/routes/analytics.js +0 -812
|
@@ -10,6 +10,83 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
10
10
|
// Batch size for database operations
|
|
11
11
|
const BATCH_INSERT_SIZE = config.QUEUE.BATCH_INSERT_SIZE;
|
|
12
12
|
|
|
13
|
+
// Performance metrics tracking for filtered pops
|
|
14
|
+
const popMetrics = {
|
|
15
|
+
partitionQueryTimes: [],
|
|
16
|
+
leaseAttempts: 0,
|
|
17
|
+
leaseSuccesses: 0,
|
|
18
|
+
messageQueryTimes: [],
|
|
19
|
+
totalPopTimes: [],
|
|
20
|
+
batchSizes: [],
|
|
21
|
+
lastLogTime: Date.now()
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Performance metrics for direct pops (queue/partition)
|
|
25
|
+
const directPopMetrics = {
|
|
26
|
+
leaseAcquisitionTimes: [],
|
|
27
|
+
messageQueryTimes: [],
|
|
28
|
+
statusUpdateTimes: [],
|
|
29
|
+
leaseUpdateTimes: [],
|
|
30
|
+
decryptionTimes: [],
|
|
31
|
+
totalPopTimes: [],
|
|
32
|
+
batchSizes: [],
|
|
33
|
+
lastLogTime: Date.now()
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Log metrics periodically (every 5 seconds OR after 10 samples)
|
|
37
|
+
const logMetrics = () => {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const timeSinceLastLog = now - Math.max(popMetrics.lastLogTime, directPopMetrics.lastLogTime);
|
|
40
|
+
const hasEnoughSamples = popMetrics.totalPopTimes.length >= 10 || directPopMetrics.totalPopTimes.length >= 10;
|
|
41
|
+
|
|
42
|
+
// Only log if enough time passed OR we have enough samples
|
|
43
|
+
if (timeSinceLastLog < 5000 && !hasEnoughSamples) return;
|
|
44
|
+
|
|
45
|
+
if (popMetrics.totalPopTimes.length === 0 && directPopMetrics.totalPopTimes.length === 0) return; // No data yet
|
|
46
|
+
|
|
47
|
+
const avg = arr => arr.length > 0 ? (arr.reduce((a,b) => a+b, 0) / arr.length).toFixed(2) : 0;
|
|
48
|
+
const max = arr => arr.length > 0 ? Math.max(...arr).toFixed(2) : 0;
|
|
49
|
+
const min = arr => arr.length > 0 ? Math.min(...arr).toFixed(2) : 0;
|
|
50
|
+
|
|
51
|
+
if (popMetrics.totalPopTimes.length > 0) {
|
|
52
|
+
log(`📊 PERF METRICS (filtered pop) | Samples: ${popMetrics.totalPopTimes.length}`);
|
|
53
|
+
log(` - Partition Query: avg=${avg(popMetrics.partitionQueryTimes)}ms max=${max(popMetrics.partitionQueryTimes)}ms`);
|
|
54
|
+
log(` - Message Query: avg=${avg(popMetrics.messageQueryTimes)}ms max=${max(popMetrics.messageQueryTimes)}ms`);
|
|
55
|
+
log(` - Total Pop: avg=${avg(popMetrics.totalPopTimes)}ms max=${max(popMetrics.totalPopTimes)}ms`);
|
|
56
|
+
log(` - Lease Success Rate: ${popMetrics.leaseSuccesses}/${popMetrics.leaseAttempts} (${(popMetrics.leaseSuccesses/popMetrics.leaseAttempts*100).toFixed(1)}%)`);
|
|
57
|
+
log(` - Avg Batch Size: ${avg(popMetrics.batchSizes)} messages`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (directPopMetrics.totalPopTimes.length > 0) {
|
|
61
|
+
log(`📊 PERF METRICS (direct pop) | Samples: ${directPopMetrics.totalPopTimes.length}`);
|
|
62
|
+
log(` - Lease Acquisition: avg=${avg(directPopMetrics.leaseAcquisitionTimes)}ms max=${max(directPopMetrics.leaseAcquisitionTimes)}ms`);
|
|
63
|
+
log(` - Message Query: avg=${avg(directPopMetrics.messageQueryTimes)}ms max=${max(directPopMetrics.messageQueryTimes)}ms`);
|
|
64
|
+
log(` - Status Update: avg=${avg(directPopMetrics.statusUpdateTimes)}ms max=${max(directPopMetrics.statusUpdateTimes)}ms`);
|
|
65
|
+
log(` - Lease Update: avg=${avg(directPopMetrics.leaseUpdateTimes)}ms max=${max(directPopMetrics.leaseUpdateTimes)}ms`);
|
|
66
|
+
log(` - Decryption/Format: avg=${avg(directPopMetrics.decryptionTimes)}ms max=${max(directPopMetrics.decryptionTimes)}ms`);
|
|
67
|
+
log(` - Total Pop: avg=${avg(directPopMetrics.totalPopTimes)}ms max=${max(directPopMetrics.totalPopTimes)}ms`);
|
|
68
|
+
log(` - Avg Batch Size: ${avg(directPopMetrics.batchSizes)} messages`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Reset metrics after logging
|
|
72
|
+
popMetrics.partitionQueryTimes = [];
|
|
73
|
+
popMetrics.leaseAttempts = 0;
|
|
74
|
+
popMetrics.leaseSuccesses = 0;
|
|
75
|
+
popMetrics.messageQueryTimes = [];
|
|
76
|
+
popMetrics.totalPopTimes = [];
|
|
77
|
+
popMetrics.batchSizes = [];
|
|
78
|
+
popMetrics.lastLogTime = now;
|
|
79
|
+
|
|
80
|
+
directPopMetrics.leaseAcquisitionTimes = [];
|
|
81
|
+
directPopMetrics.messageQueryTimes = [];
|
|
82
|
+
directPopMetrics.statusUpdateTimes = [];
|
|
83
|
+
directPopMetrics.leaseUpdateTimes = [];
|
|
84
|
+
directPopMetrics.decryptionTimes = [];
|
|
85
|
+
directPopMetrics.totalPopTimes = [];
|
|
86
|
+
directPopMetrics.batchSizes = [];
|
|
87
|
+
directPopMetrics.lastLogTime = now;
|
|
88
|
+
};
|
|
89
|
+
|
|
13
90
|
// Ensure queue exists and partition exist (with caching)
|
|
14
91
|
// Queue must be created via configure endpoint, but partitions can be created on-demand
|
|
15
92
|
const ensureResources = async (client, queueName, partitionName = 'Default', namespace = null, task = null) => {
|
|
@@ -126,6 +203,8 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
126
203
|
|
|
127
204
|
// Optimized batch insert for high throughput (simplified - no status)
|
|
128
205
|
const pushMessagesBatch = async (items) => {
|
|
206
|
+
const pushStartTime = Date.now();
|
|
207
|
+
|
|
129
208
|
// Group items by queue and partition for efficient resource lookup
|
|
130
209
|
const partitionGroups = {};
|
|
131
210
|
for (const item of items) {
|
|
@@ -139,6 +218,45 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
139
218
|
|
|
140
219
|
const allResults = [];
|
|
141
220
|
|
|
221
|
+
// OPTIMIZATION: Check queue capacity once per queue, not per partition
|
|
222
|
+
// Build a map of queue -> total batch size across all partitions
|
|
223
|
+
const queueBatchSizes = {};
|
|
224
|
+
for (const [partitionKey, partitionItems] of Object.entries(partitionGroups)) {
|
|
225
|
+
const [queueName] = partitionKey.split(':');
|
|
226
|
+
queueBatchSizes[queueName] = (queueBatchSizes[queueName] || 0) + partitionItems.length;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check capacity for all queues with max_queue_size set (single query)
|
|
230
|
+
const queuesToCheck = Object.keys(queueBatchSizes);
|
|
231
|
+
if (queuesToCheck.length > 0) {
|
|
232
|
+
await withTransaction(pool, async (client) => {
|
|
233
|
+
const capacityCheck = await client.query(
|
|
234
|
+
`SELECT
|
|
235
|
+
q.name,
|
|
236
|
+
q.max_queue_size,
|
|
237
|
+
COALESCE(SUM(pc.pending_estimate), 0)::integer as current_depth
|
|
238
|
+
FROM queen.queues q
|
|
239
|
+
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
240
|
+
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
241
|
+
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
242
|
+
WHERE q.name = ANY($1::varchar[])
|
|
243
|
+
AND q.max_queue_size > 0
|
|
244
|
+
GROUP BY q.name, q.max_queue_size`,
|
|
245
|
+
[queuesToCheck]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Check each queue's capacity
|
|
249
|
+
for (const row of capacityCheck.rows) {
|
|
250
|
+
const currentDepth = parseInt(row.current_depth);
|
|
251
|
+
const batchSize = queueBatchSizes[row.name];
|
|
252
|
+
const maxSize = parseInt(row.max_queue_size);
|
|
253
|
+
if (currentDepth + batchSize > maxSize) {
|
|
254
|
+
throw new Error(`Queue '${row.name}' would exceed max capacity (${maxSize}). Current: ${currentDepth}, Batch: ${batchSize}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
142
260
|
// Process each partition group
|
|
143
261
|
for (const [partitionKey, partitionItems] of Object.entries(partitionGroups)) {
|
|
144
262
|
const [queueName, partitionName] = partitionKey.split(':');
|
|
@@ -154,27 +272,6 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
154
272
|
const resources = await ensureResources(client, queueName, partitionName);
|
|
155
273
|
const { partitionId, encryptionEnabled, queueConfig } = resources;
|
|
156
274
|
|
|
157
|
-
// Check queue capacity if max_queue_size is set
|
|
158
|
-
if (queueConfig.maxQueueSize > 0) {
|
|
159
|
-
const capacityCheck = await client.query(
|
|
160
|
-
`SELECT COUNT(m.id) as current_depth
|
|
161
|
-
FROM queen.messages m
|
|
162
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
163
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
164
|
-
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
|
|
165
|
-
AND ms.consumer_group = '__QUEUE_MODE__'
|
|
166
|
-
WHERE q.name = $1
|
|
167
|
-
AND (ms.status IS NULL OR ms.status IN ('pending', 'processing'))`,
|
|
168
|
-
[queueName]
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const currentDepth = parseInt(capacityCheck.rows[0].current_depth);
|
|
172
|
-
const batchSize = partitionItems.length;
|
|
173
|
-
if (currentDepth + batchSize > queueConfig.maxQueueSize) {
|
|
174
|
-
throw new Error(`Queue '${queueName}' would exceed max capacity (${queueConfig.maxQueueSize}). Current: ${currentDepth}, Batch: ${batchSize}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
275
|
// Process items in batches
|
|
179
276
|
const batchResults = [];
|
|
180
277
|
|
|
@@ -189,29 +286,39 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
189
286
|
const encryptedFlags = [];
|
|
190
287
|
const duplicates = [];
|
|
191
288
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
289
|
+
// ⚡ OPTIMIZATION #1: Batch duplicate check - single query for all transaction IDs
|
|
290
|
+
const allTransactionIds = batch.map(item => item.transactionId || generateUUID());
|
|
291
|
+
|
|
292
|
+
// Skip duplicate check if no transaction IDs were explicitly provided (all generated)
|
|
293
|
+
const hasExplicitTxnIds = batch.some(item => item.transactionId);
|
|
294
|
+
let existingTxnIds = new Map();
|
|
295
|
+
|
|
296
|
+
if (hasExplicitTxnIds) {
|
|
297
|
+
// Use more efficient LEFT JOIN approach instead of ANY() for better index usage
|
|
198
298
|
const dupCheck = await client.query(
|
|
199
|
-
|
|
200
|
-
|
|
299
|
+
`SELECT t.txn_id as transaction_id, m.id
|
|
300
|
+
FROM UNNEST($1::varchar[]) AS t(txn_id)
|
|
301
|
+
LEFT JOIN queen.messages m ON m.transaction_id = t.txn_id
|
|
302
|
+
WHERE m.id IS NOT NULL`,
|
|
303
|
+
[allTransactionIds]
|
|
201
304
|
);
|
|
202
305
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
306
|
+
// Create a map of existing transaction IDs for O(1) lookup
|
|
307
|
+
existingTxnIds = new Map(dupCheck.rows.map(row => [row.transaction_id, row.id]));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ⚡ OPTIMIZATION #2: Parallel encryption - encrypt all payloads concurrently
|
|
311
|
+
const encryptionTasks = batch.map(async (item, idx) => {
|
|
312
|
+
const transactionId = allTransactionIds[idx];
|
|
211
313
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
314
|
+
// Check if this transaction ID already exists
|
|
315
|
+
if (existingTxnIds.has(transactionId)) {
|
|
316
|
+
return {
|
|
317
|
+
isDuplicate: true,
|
|
318
|
+
id: existingTxnIds.get(transactionId),
|
|
319
|
+
transactionId
|
|
320
|
+
};
|
|
321
|
+
}
|
|
215
322
|
|
|
216
323
|
// Handle encryption if enabled
|
|
217
324
|
let payload = item.payload;
|
|
@@ -222,34 +329,61 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
222
329
|
payload = await encryption.encryptPayload(item.payload);
|
|
223
330
|
isEncrypted = true;
|
|
224
331
|
} catch (error) {
|
|
225
|
-
log(`ERROR: Encryption failed for
|
|
332
|
+
log(`ERROR: Encryption failed for transaction ${transactionId}:`, error);
|
|
226
333
|
// Continue with unencrypted payload
|
|
227
334
|
}
|
|
228
335
|
}
|
|
229
336
|
|
|
337
|
+
return {
|
|
338
|
+
isDuplicate: false,
|
|
339
|
+
messageId: generateUUID(),
|
|
340
|
+
transactionId,
|
|
341
|
+
traceId: item.traceId || null,
|
|
342
|
+
payload,
|
|
343
|
+
isEncrypted
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Wait for all encryption tasks to complete in parallel
|
|
348
|
+
const encryptionResults = await Promise.all(encryptionTasks);
|
|
349
|
+
|
|
350
|
+
// Process results
|
|
351
|
+
for (const result of encryptionResults) {
|
|
352
|
+
if (result.isDuplicate) {
|
|
353
|
+
duplicates.push({
|
|
354
|
+
id: result.id,
|
|
355
|
+
transactionId: result.transactionId,
|
|
356
|
+
status: 'duplicate'
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
230
361
|
// Handle large payloads and special cases
|
|
231
362
|
let jsonPayload;
|
|
232
363
|
try {
|
|
233
364
|
// Handle null/undefined payloads - convert to JSON null string
|
|
234
|
-
if (payload === null || payload === undefined) {
|
|
365
|
+
if (result.payload === null || result.payload === undefined) {
|
|
235
366
|
jsonPayload = 'null'; // JSONB expects the string 'null' for null values
|
|
236
367
|
} else {
|
|
237
368
|
// Try to stringify the payload
|
|
238
|
-
jsonPayload = JSON.stringify(payload);
|
|
369
|
+
jsonPayload = JSON.stringify(result.payload);
|
|
239
370
|
}
|
|
240
371
|
} catch (error) {
|
|
241
372
|
// If JSON.stringify fails (e.g., circular references, too large),
|
|
242
373
|
// store as a string representation
|
|
243
|
-
log(`WARN: Failed to stringify payload for message ${messageId}:`, error.message);
|
|
374
|
+
log(`WARN: Failed to stringify payload for message ${result.messageId}:`, error.message);
|
|
244
375
|
jsonPayload = JSON.stringify({
|
|
245
376
|
error: 'Payload serialization failed',
|
|
246
|
-
type: typeof payload,
|
|
377
|
+
type: typeof result.payload,
|
|
247
378
|
message: error.message
|
|
248
379
|
});
|
|
249
380
|
}
|
|
250
381
|
|
|
382
|
+
messageIds.push(result.messageId);
|
|
383
|
+
transactionIds.push(result.transactionId);
|
|
384
|
+
traceIds.push(result.traceId);
|
|
251
385
|
payloads.push(jsonPayload);
|
|
252
|
-
encryptedFlags.push(isEncrypted);
|
|
386
|
+
encryptedFlags.push(result.isEncrypted);
|
|
253
387
|
}
|
|
254
388
|
|
|
255
389
|
// Batch insert messages with trace_id
|
|
@@ -316,601 +450,24 @@ export const createOptimizedQueueManager = (pool, resourceCache, eventManager, s
|
|
|
316
450
|
}
|
|
317
451
|
}
|
|
318
452
|
|
|
453
|
+
const duration = Date.now() - pushStartTime;
|
|
454
|
+
log(`[PUSH] Pushed ${items.length} items → ${allResults.length} results in ${duration}ms (${(items.length / (duration / 1000)).toFixed(0)} msg/s)`);
|
|
319
455
|
return allResults;
|
|
320
456
|
};
|
|
321
457
|
|
|
322
|
-
// Helper function to ensure consumer group exists
|
|
323
|
-
const ensureConsumerGroup = async (client, queueId, consumerGroup, subscriptionMode, subscriptionFrom) => {
|
|
324
|
-
// Handle special subscription values
|
|
325
|
-
let effectiveSubscriptionFrom = subscriptionFrom;
|
|
326
|
-
if (subscriptionFrom === 'now' || subscriptionMode === 'new' || subscriptionMode === 'new-only') {
|
|
327
|
-
effectiveSubscriptionFrom = new Date();
|
|
328
|
-
} else if (!subscriptionFrom || subscriptionFrom === 'all') {
|
|
329
|
-
effectiveSubscriptionFrom = null; // Will consume all messages
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const result = await client.query(`
|
|
333
|
-
INSERT INTO queen.consumer_groups (queue_id, name, subscription_start_from)
|
|
334
|
-
VALUES ($1, $2, $3)
|
|
335
|
-
ON CONFLICT (queue_id, name) DO UPDATE SET
|
|
336
|
-
subscription_start_from = CASE
|
|
337
|
-
WHEN queen.consumer_groups.subscription_start_from IS NULL
|
|
338
|
-
THEN EXCLUDED.subscription_start_from
|
|
339
|
-
ELSE queen.consumer_groups.subscription_start_from
|
|
340
|
-
END,
|
|
341
|
-
last_seen_at = NOW()
|
|
342
|
-
RETURNING *
|
|
343
|
-
`, [queueId, consumerGroup, effectiveSubscriptionFrom]);
|
|
344
|
-
|
|
345
|
-
return result.rows[0];
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
// Helper function for eviction on pop
|
|
349
|
-
const evictOnPop = async (client, queueName) => {
|
|
350
|
-
const evictionResult = await client.query(`
|
|
351
|
-
WITH queue_config AS (
|
|
352
|
-
SELECT id, max_wait_time_seconds
|
|
353
|
-
FROM queen.queues
|
|
354
|
-
WHERE name = $1 AND max_wait_time_seconds > 0
|
|
355
|
-
),
|
|
356
|
-
evicted AS (
|
|
357
|
-
DELETE FROM queen.messages
|
|
358
|
-
WHERE id IN (
|
|
359
|
-
SELECT m.id
|
|
360
|
-
FROM queen.messages m
|
|
361
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
362
|
-
JOIN queue_config qc ON p.queue_id = qc.id
|
|
363
|
-
WHERE m.created_at < NOW() - INTERVAL '1 second' * qc.max_wait_time_seconds
|
|
364
|
-
)
|
|
365
|
-
RETURNING id
|
|
366
|
-
)
|
|
367
|
-
SELECT COUNT(*) as count FROM evicted
|
|
368
|
-
`, [queueName]);
|
|
369
|
-
|
|
370
|
-
if (evictionResult.rows[0].count > 0) {
|
|
371
|
-
log(`${LogTypes.POP} | EVICTION | Queue: ${queueName} | Count: ${evictionResult.rows[0].count}`);
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Pop messages with simplified transaction-based approach
|
|
377
|
-
* This is a drop-in replacement for the original popMessages function
|
|
378
|
-
*/
|
|
379
|
-
const popMessagesV2 = async (scope, options = {}) => {
|
|
380
|
-
const { queue, partition, consumerGroup } = scope;
|
|
381
|
-
const {
|
|
382
|
-
wait = false,
|
|
383
|
-
timeout = config.QUEUE.DEFAULT_TIMEOUT,
|
|
384
|
-
batch = config.QUEUE.DEFAULT_BATCH_SIZE,
|
|
385
|
-
subscriptionMode = null,
|
|
386
|
-
subscriptionFrom = null
|
|
387
|
-
} = options;
|
|
388
|
-
|
|
389
|
-
return withTransaction(pool, async (client) => {
|
|
390
|
-
// Set transaction isolation level to prevent dirty reads
|
|
391
|
-
await client.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
|
|
392
|
-
|
|
393
|
-
// Step 1: Reclaim expired leases
|
|
394
|
-
const reclaimResult = await client.query(`
|
|
395
|
-
UPDATE queen.messages_status
|
|
396
|
-
SET status = 'pending',
|
|
397
|
-
lease_expires_at = NULL,
|
|
398
|
-
worker_id = NULL,
|
|
399
|
-
processing_at = NULL
|
|
400
|
-
WHERE status = 'processing'
|
|
401
|
-
AND lease_expires_at < NOW()
|
|
402
|
-
AND consumer_group = $1
|
|
403
|
-
RETURNING message_id
|
|
404
|
-
`, [consumerGroup || '__QUEUE_MODE__']);
|
|
405
|
-
|
|
406
|
-
if (reclaimResult.rowCount > 0) {
|
|
407
|
-
log(`Reclaimed ${reclaimResult.rowCount} expired leases`);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Step 2: Evict expired messages
|
|
411
|
-
await evictOnPop(client, queue);
|
|
412
|
-
|
|
413
|
-
// Step 3: Get queue information
|
|
414
|
-
const queueResult = await client.query(
|
|
415
|
-
'SELECT * FROM queen.queues WHERE name = $1',
|
|
416
|
-
[queue]
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
if (queueResult.rows.length === 0) {
|
|
420
|
-
return { messages: [] };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const queueInfo = queueResult.rows[0];
|
|
424
|
-
|
|
425
|
-
// Step 4: Handle consumer group if specified
|
|
426
|
-
let consumerGroupInfo = null;
|
|
427
|
-
if (consumerGroup) {
|
|
428
|
-
consumerGroupInfo = await ensureConsumerGroup(
|
|
429
|
-
client,
|
|
430
|
-
queueInfo.id,
|
|
431
|
-
consumerGroup,
|
|
432
|
-
subscriptionMode,
|
|
433
|
-
subscriptionFrom
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
438
|
-
let subscriptionStart = consumerGroupInfo?.subscription_start_from;
|
|
439
|
-
|
|
440
|
-
// Handle subscription start date
|
|
441
|
-
if (consumerGroup) {
|
|
442
|
-
if (!subscriptionStart) {
|
|
443
|
-
subscriptionStart = '1970-01-01';
|
|
444
|
-
} else if (typeof subscriptionStart === 'object' && subscriptionStart instanceof Date) {
|
|
445
|
-
subscriptionStart = subscriptionStart.toISOString();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Step 5: Get or create partition
|
|
450
|
-
let partitionInfo;
|
|
451
|
-
if (partition) {
|
|
452
|
-
// Specific partition requested
|
|
453
|
-
const partitionResult = await client.query(`
|
|
454
|
-
SELECT p.* FROM queen.partitions p
|
|
455
|
-
WHERE p.queue_id = $1 AND p.name = $2
|
|
456
|
-
`, [queueInfo.id, partition]);
|
|
457
|
-
|
|
458
|
-
if (partitionResult.rows.length === 0) {
|
|
459
|
-
// Create partition if it doesn't exist
|
|
460
|
-
const createPartitionResult = await client.query(`
|
|
461
|
-
INSERT INTO queen.partitions (id, queue_id, name)
|
|
462
|
-
VALUES ($1, $2, $3)
|
|
463
|
-
ON CONFLICT (queue_id, name) DO UPDATE SET name = EXCLUDED.name
|
|
464
|
-
RETURNING *
|
|
465
|
-
`, [generateUUID(), queueInfo.id, partition]);
|
|
466
|
-
partitionInfo = createPartitionResult.rows[0];
|
|
467
|
-
} else {
|
|
468
|
-
partitionInfo = partitionResult.rows[0];
|
|
469
|
-
}
|
|
470
|
-
} else {
|
|
471
|
-
// Find any available partition with messages that is not currently leased
|
|
472
|
-
// This applies to both QUEUE MODE and BUS MODE for partition isolation
|
|
473
|
-
const partitionResult = await client.query(`
|
|
474
|
-
SELECT DISTINCT p.*
|
|
475
|
-
FROM queen.partitions p
|
|
476
|
-
JOIN queen.messages m ON m.partition_id = p.id
|
|
477
|
-
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
|
|
478
|
-
AND ms.consumer_group = $2
|
|
479
|
-
LEFT JOIN queen.partition_leases pl ON p.id = pl.partition_id
|
|
480
|
-
AND pl.consumer_group = $2
|
|
481
|
-
AND pl.released_at IS NULL
|
|
482
|
-
AND pl.lease_expires_at > NOW()
|
|
483
|
-
WHERE p.queue_id = $1
|
|
484
|
-
AND (ms.id IS NULL OR ms.status IN ('pending', 'failed'))
|
|
485
|
-
AND pl.id IS NULL -- No active lease for this partition
|
|
486
|
-
ORDER BY p.created_at
|
|
487
|
-
LIMIT 1
|
|
488
|
-
`, [queueInfo.id, actualConsumerGroup]);
|
|
489
|
-
|
|
490
|
-
if (partitionResult.rows.length === 0) {
|
|
491
|
-
// No partitions with available messages, check if we need to create default
|
|
492
|
-
const anyPartitionResult = await client.query(`
|
|
493
|
-
SELECT * FROM queen.partitions WHERE queue_id = $1 LIMIT 1
|
|
494
|
-
`, [queueInfo.id]);
|
|
495
|
-
|
|
496
|
-
if (anyPartitionResult.rows.length === 0) {
|
|
497
|
-
// Create default partition
|
|
498
|
-
const createPartitionResult = await client.query(`
|
|
499
|
-
INSERT INTO queen.partitions (id, queue_id, name)
|
|
500
|
-
VALUES ($1, $2, 'Default')
|
|
501
|
-
ON CONFLICT (queue_id, name) DO UPDATE SET name = EXCLUDED.name
|
|
502
|
-
RETURNING *
|
|
503
|
-
`, [generateUUID(), queueInfo.id]);
|
|
504
|
-
partitionInfo = createPartitionResult.rows[0];
|
|
505
|
-
} else {
|
|
506
|
-
return { messages: [] };
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
partitionInfo = partitionResult.rows[0];
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Step 6: Try to acquire or verify partition lease (for both QUEUE MODE and BUS MODE)
|
|
514
|
-
// This ensures partition isolation between consumers in the same group
|
|
515
|
-
|
|
516
|
-
// First, try to acquire a new lease or check if we can take over an expired one
|
|
517
|
-
const leaseResult = await client.query(`
|
|
518
|
-
INSERT INTO queen.partition_leases (
|
|
519
|
-
partition_id,
|
|
520
|
-
consumer_group,
|
|
521
|
-
lease_expires_at
|
|
522
|
-
) VALUES ($1, $2, NOW() + INTERVAL '1 second' * $3)
|
|
523
|
-
ON CONFLICT (partition_id, consumer_group)
|
|
524
|
-
DO UPDATE SET
|
|
525
|
-
lease_expires_at = CASE
|
|
526
|
-
WHEN queen.partition_leases.released_at IS NOT NULL
|
|
527
|
-
OR queen.partition_leases.lease_expires_at <= NOW()
|
|
528
|
-
THEN EXCLUDED.lease_expires_at
|
|
529
|
-
ELSE queen.partition_leases.lease_expires_at
|
|
530
|
-
END,
|
|
531
|
-
released_at = CASE
|
|
532
|
-
WHEN queen.partition_leases.released_at IS NOT NULL
|
|
533
|
-
OR queen.partition_leases.lease_expires_at <= NOW()
|
|
534
|
-
THEN NULL
|
|
535
|
-
ELSE queen.partition_leases.released_at
|
|
536
|
-
END
|
|
537
|
-
RETURNING
|
|
538
|
-
lease_expires_at,
|
|
539
|
-
(lease_expires_at = NOW() + INTERVAL '1 second' * $3) as acquired
|
|
540
|
-
`, [partitionInfo.id, actualConsumerGroup, queueInfo.lease_time]);
|
|
541
|
-
|
|
542
|
-
// Check if we actually acquired the lease
|
|
543
|
-
if (!leaseResult.rows[0].acquired) {
|
|
544
|
-
// Another consumer still has an active lease on this partition
|
|
545
|
-
return { messages: [] };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Step 7: Build WHERE clause based on mode
|
|
549
|
-
let whereClause;
|
|
550
|
-
let queryParams;
|
|
551
|
-
|
|
552
|
-
if (consumerGroup) {
|
|
553
|
-
// BUS MODE with consumer group
|
|
554
|
-
whereClause = `
|
|
555
|
-
m.partition_id = $2
|
|
556
|
-
AND (ms.id IS NULL OR ms.status IN ('pending', 'failed'))
|
|
557
|
-
AND m.created_at <= NOW() - INTERVAL '1 second' * $3
|
|
558
|
-
AND m.created_at >= $4::timestamp
|
|
559
|
-
AND ($5 = 0 OR m.created_at > NOW() - INTERVAL '1 second' * $5)
|
|
560
|
-
`;
|
|
561
|
-
queryParams = [
|
|
562
|
-
actualConsumerGroup,
|
|
563
|
-
partitionInfo.id,
|
|
564
|
-
queueInfo.delayed_processing || 0,
|
|
565
|
-
subscriptionStart,
|
|
566
|
-
queueInfo.max_wait_time_seconds || 0,
|
|
567
|
-
batch
|
|
568
|
-
];
|
|
569
|
-
} else {
|
|
570
|
-
// QUEUE MODE
|
|
571
|
-
whereClause = `
|
|
572
|
-
m.partition_id = $2
|
|
573
|
-
AND (ms.id IS NULL OR ms.status IN ('pending', 'failed'))
|
|
574
|
-
AND m.created_at <= NOW() - INTERVAL '1 second' * $3
|
|
575
|
-
AND ($4 = 0 OR m.created_at > NOW() - INTERVAL '1 second' * $4)
|
|
576
|
-
AND ($5 = 0 OR NOT EXISTS (
|
|
577
|
-
SELECT 1 FROM queen.messages m2
|
|
578
|
-
WHERE m2.partition_id = p.id
|
|
579
|
-
AND m2.created_at > NOW() - INTERVAL '1 second' * $5
|
|
580
|
-
))
|
|
581
|
-
`;
|
|
582
|
-
queryParams = [
|
|
583
|
-
actualConsumerGroup,
|
|
584
|
-
partitionInfo.id,
|
|
585
|
-
queueInfo.delayed_processing || 0,
|
|
586
|
-
queueInfo.max_wait_time_seconds || 0,
|
|
587
|
-
queueInfo.window_buffer || 0,
|
|
588
|
-
batch
|
|
589
|
-
];
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Step 8: Select and lock messages
|
|
593
|
-
const messagesResult = await client.query(`
|
|
594
|
-
SELECT
|
|
595
|
-
m.id,
|
|
596
|
-
m.transaction_id,
|
|
597
|
-
m.trace_id,
|
|
598
|
-
m.payload,
|
|
599
|
-
m.is_encrypted,
|
|
600
|
-
m.created_at,
|
|
601
|
-
p.name as partition_name,
|
|
602
|
-
q.name as queue_name,
|
|
603
|
-
q.priority,
|
|
604
|
-
COALESCE(ms.retry_count, 0) as retry_count
|
|
605
|
-
FROM queen.messages m
|
|
606
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
607
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
608
|
-
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
|
|
609
|
-
AND ms.consumer_group = $1
|
|
610
|
-
WHERE ${whereClause}
|
|
611
|
-
ORDER BY m.created_at ASC
|
|
612
|
-
LIMIT $6
|
|
613
|
-
FOR UPDATE OF m NOWAIT
|
|
614
|
-
`, queryParams).catch(err => {
|
|
615
|
-
if (err.code === '55P03') { // Lock not available
|
|
616
|
-
return { rows: [] };
|
|
617
|
-
}
|
|
618
|
-
throw err;
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
const messages = messagesResult.rows;
|
|
622
|
-
|
|
623
|
-
if (messages.length === 0) {
|
|
624
|
-
// Release partition lease if no messages
|
|
625
|
-
await client.query(`
|
|
626
|
-
UPDATE queen.partition_leases
|
|
627
|
-
SET released_at = NOW()
|
|
628
|
-
WHERE partition_id = $1 AND consumer_group = $2
|
|
629
|
-
`, [partitionInfo.id, actualConsumerGroup]);
|
|
630
|
-
return { messages: [] };
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Step 9: Update or insert message status for selected messages
|
|
634
|
-
const leaseExpiresAt = new Date(Date.now() + (queueInfo.lease_time * 1000));
|
|
635
|
-
|
|
636
|
-
for (const message of messages) {
|
|
637
|
-
await client.query(`
|
|
638
|
-
INSERT INTO queen.messages_status (
|
|
639
|
-
message_id,
|
|
640
|
-
consumer_group,
|
|
641
|
-
status,
|
|
642
|
-
lease_expires_at,
|
|
643
|
-
processing_at,
|
|
644
|
-
worker_id,
|
|
645
|
-
retry_count
|
|
646
|
-
) VALUES ($1, $2, 'processing', $3, NOW(), $4, $5)
|
|
647
|
-
ON CONFLICT (message_id, consumer_group)
|
|
648
|
-
DO UPDATE SET
|
|
649
|
-
status = 'processing',
|
|
650
|
-
lease_expires_at = EXCLUDED.lease_expires_at,
|
|
651
|
-
processing_at = NOW(),
|
|
652
|
-
worker_id = EXCLUDED.worker_id
|
|
653
|
-
-- Keep existing retry_count when re-processing failed messages
|
|
654
|
-
`, [
|
|
655
|
-
message.id,
|
|
656
|
-
actualConsumerGroup,
|
|
657
|
-
leaseExpiresAt,
|
|
658
|
-
config.WORKER_ID,
|
|
659
|
-
message.retry_count || 0
|
|
660
|
-
]);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Step 10: Update partition lease with message batch
|
|
664
|
-
if (messages.length > 0) {
|
|
665
|
-
const messageIds = messages.map(m => m.id);
|
|
666
|
-
await client.query(`
|
|
667
|
-
UPDATE queen.partition_leases
|
|
668
|
-
SET message_batch = $1::jsonb
|
|
669
|
-
WHERE partition_id = $2 AND consumer_group = $3
|
|
670
|
-
`, [JSON.stringify(messageIds), partitionInfo.id, actualConsumerGroup]);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Step 11: Decrypt payloads if needed and format response
|
|
674
|
-
const decryptedMessages = await Promise.all(messages.map(async (msg) => {
|
|
675
|
-
let payload = msg.payload;
|
|
676
|
-
if (msg.is_encrypted && encryption.isEncryptionEnabled()) {
|
|
677
|
-
try {
|
|
678
|
-
payload = await encryption.decryptPayload(msg.payload);
|
|
679
|
-
} catch (error) {
|
|
680
|
-
log(`${LogTypes.ERROR} | Failed to decrypt message ${msg.transaction_id}: ${error.message}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return {
|
|
685
|
-
id: msg.id,
|
|
686
|
-
transactionId: msg.transaction_id,
|
|
687
|
-
traceId: msg.trace_id,
|
|
688
|
-
queue: msg.queue_name,
|
|
689
|
-
partition: msg.partition_name,
|
|
690
|
-
data: payload,
|
|
691
|
-
payload: payload,
|
|
692
|
-
retryCount: msg.retry_count || 0,
|
|
693
|
-
priority: msg.priority || 0,
|
|
694
|
-
createdAt: msg.created_at,
|
|
695
|
-
consumerGroup: consumerGroup || null
|
|
696
|
-
};
|
|
697
|
-
}));
|
|
698
|
-
|
|
699
|
-
log(`${LogTypes.POP} | Count: ${decryptedMessages.length} | ConsumerGroup: ${actualConsumerGroup === '__QUEUE_MODE__' ? 'QUEUE_MODE' : actualConsumerGroup}`);
|
|
700
|
-
|
|
701
|
-
return { messages: decryptedMessages };
|
|
702
|
-
});
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const popMessages = async (scope, options = {}) => {
|
|
707
|
-
return popMessagesV2(scope, options);
|
|
708
|
-
};
|
|
709
458
|
|
|
710
459
|
// Acknowledge messages (now per consumer group)
|
|
460
|
+
// CURSOR-BASED: Single ACK now routes through batch ACK to advance cursor properly
|
|
711
461
|
const acknowledgeMessage = async (transactionId, status = 'completed', error = null, consumerGroup = null) => {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const findQuery = consumerGroup
|
|
715
|
-
? `SELECT ms.retry_count, ms.status, ms.message_id, m.partition_id,
|
|
716
|
-
q.retry_limit, q.dlq_after_max_retries
|
|
717
|
-
FROM queen.messages_status ms
|
|
718
|
-
JOIN queen.messages m ON ms.message_id = m.id
|
|
719
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
720
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
721
|
-
WHERE m.transaction_id = $1 AND ms.consumer_group = $2 AND ms.status = 'processing'`
|
|
722
|
-
: `SELECT ms.retry_count, ms.status, ms.message_id, m.partition_id,
|
|
723
|
-
q.retry_limit, q.dlq_after_max_retries
|
|
724
|
-
FROM queen.messages_status ms
|
|
725
|
-
JOIN queen.messages m ON ms.message_id = m.id
|
|
726
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
727
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
728
|
-
WHERE m.transaction_id = $1 AND ms.consumer_group = '__QUEUE_MODE__' AND ms.status = 'processing'`;
|
|
729
|
-
|
|
730
|
-
const params = consumerGroup ? [transactionId, consumerGroup] : [transactionId];
|
|
731
|
-
const result = await client.query(findQuery, params);
|
|
732
|
-
|
|
733
|
-
if (result.rows.length === 0) {
|
|
734
|
-
log(`WARN: Message not found for acknowledgment: ${transactionId}, consumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
735
|
-
|
|
736
|
-
// Try to find the message regardless of status for debugging
|
|
737
|
-
const debugQuery = consumerGroup
|
|
738
|
-
? `SELECT ms.retry_count, ms.status FROM queen.messages_status ms
|
|
739
|
-
JOIN queen.messages m ON ms.message_id = m.id
|
|
740
|
-
WHERE m.transaction_id = $1 AND ms.consumer_group = $2`
|
|
741
|
-
: `SELECT ms.retry_count, ms.status FROM queen.messages_status ms
|
|
742
|
-
JOIN queen.messages m ON ms.message_id = m.id
|
|
743
|
-
WHERE m.transaction_id = $1 AND ms.consumer_group = '__QUEUE_MODE__'`;
|
|
744
|
-
const debugResult = await client.query(debugQuery, params);
|
|
745
|
-
if (debugResult.rows.length > 0) {
|
|
746
|
-
log(`DEBUG: Found message with status=${debugResult.rows[0].status}, retry_count=${debugResult.rows[0].retry_count}`);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return { status: 'not_found', transaction_id: transactionId };
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const messageStatus = result.rows[0];
|
|
753
|
-
log(`DEBUG: Found message for ack with retry_count=${messageStatus.retry_count}, status=${messageStatus.status}, dlq=${messageStatus.dlq_after_max_retries}, limit=${messageStatus.retry_limit}`);
|
|
754
|
-
|
|
755
|
-
if (status === 'completed') {
|
|
756
|
-
// Mark as completed and release partition lease
|
|
757
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
758
|
-
const updateParams = consumerGroup
|
|
759
|
-
? [transactionId, consumerGroup]
|
|
760
|
-
: [transactionId];
|
|
761
|
-
|
|
762
|
-
const updateQuery = consumerGroup
|
|
763
|
-
? `UPDATE queen.messages_status ms
|
|
764
|
-
SET status = 'completed', completed_at = NOW()
|
|
765
|
-
FROM queen.messages m
|
|
766
|
-
WHERE ms.message_id = m.id
|
|
767
|
-
AND m.transaction_id = $1
|
|
768
|
-
AND ms.consumer_group = $2`
|
|
769
|
-
: `UPDATE queen.messages_status ms
|
|
770
|
-
SET status = 'completed', completed_at = NOW()
|
|
771
|
-
FROM queen.messages m
|
|
772
|
-
WHERE ms.message_id = m.id
|
|
773
|
-
AND m.transaction_id = $1
|
|
774
|
-
AND ms.consumer_group = '__QUEUE_MODE__'`;
|
|
775
|
-
|
|
776
|
-
await client.query(updateQuery, updateParams);
|
|
777
|
-
|
|
778
|
-
// Release partition lease if this was the last message in the batch
|
|
779
|
-
const releaseQuery = `
|
|
780
|
-
WITH message_partition AS (
|
|
781
|
-
-- Get the partition for this message
|
|
782
|
-
SELECT DISTINCT m.partition_id
|
|
783
|
-
FROM queen.messages m
|
|
784
|
-
WHERE m.transaction_id = $1
|
|
785
|
-
),
|
|
786
|
-
check_remaining AS (
|
|
787
|
-
-- Check if there are any other processing messages in this partition
|
|
788
|
-
SELECT
|
|
789
|
-
mp.partition_id,
|
|
790
|
-
COUNT(ms.message_id) as processing_count
|
|
791
|
-
FROM message_partition mp
|
|
792
|
-
LEFT JOIN queen.messages m ON m.partition_id = mp.partition_id
|
|
793
|
-
LEFT JOIN queen.messages_status ms ON ms.message_id = m.id
|
|
794
|
-
AND ms.consumer_group = $2
|
|
795
|
-
AND ms.status = 'processing'
|
|
796
|
-
GROUP BY mp.partition_id
|
|
797
|
-
)
|
|
798
|
-
-- Release the lease only if no messages are processing
|
|
799
|
-
UPDATE queen.partition_leases pl
|
|
800
|
-
SET released_at = NOW()
|
|
801
|
-
FROM check_remaining cr
|
|
802
|
-
WHERE pl.partition_id = cr.partition_id
|
|
803
|
-
AND pl.consumer_group = $2
|
|
804
|
-
AND pl.released_at IS NULL
|
|
805
|
-
AND cr.processing_count = 0`;
|
|
806
|
-
|
|
807
|
-
await client.query(releaseQuery, [transactionId, actualConsumerGroup]);
|
|
808
|
-
|
|
809
|
-
log(`${LogTypes.ACK} | TransactionId: ${transactionId} | Status: completed | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
810
|
-
} else if (status === 'failed') {
|
|
811
|
-
// Handle failure with retry logic
|
|
812
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
813
|
-
const currentRetryCount = messageStatus.retry_count || 0;
|
|
814
|
-
const nextRetryCount = currentRetryCount + 1;
|
|
815
|
-
const retryLimit = messageStatus.retry_limit || config.QUEUE.DEFAULT_RETRY_LIMIT;
|
|
816
|
-
const dlqEnabled = messageStatus.dlq_after_max_retries;
|
|
817
|
-
|
|
818
|
-
log(`${LogTypes.ACK} | Retry check: current=${currentRetryCount}, next=${nextRetryCount}, limit=${retryLimit}, dlq=${dlqEnabled}`);
|
|
819
|
-
|
|
820
|
-
if (nextRetryCount > retryLimit && dlqEnabled) {
|
|
821
|
-
// Move to dead letter queue after exceeding retry limit
|
|
822
|
-
const updateParams = consumerGroup
|
|
823
|
-
? [transactionId, error, consumerGroup]
|
|
824
|
-
: [transactionId, error];
|
|
825
|
-
|
|
826
|
-
const updateQuery = consumerGroup
|
|
827
|
-
? `UPDATE queen.messages_status ms
|
|
828
|
-
SET status = 'dead_letter', failed_at = NOW(), error_message = $2
|
|
829
|
-
FROM queen.messages m
|
|
830
|
-
WHERE ms.message_id = m.id
|
|
831
|
-
AND m.transaction_id = $1
|
|
832
|
-
AND ms.consumer_group = $3`
|
|
833
|
-
: `UPDATE queen.messages_status ms
|
|
834
|
-
SET status = 'dead_letter', failed_at = NOW(), error_message = $2
|
|
835
|
-
FROM queen.messages m
|
|
836
|
-
WHERE ms.message_id = m.id
|
|
837
|
-
AND m.transaction_id = $1
|
|
838
|
-
AND ms.consumer_group = '__QUEUE_MODE__'`;
|
|
839
|
-
|
|
840
|
-
await client.query(updateQuery, updateParams);
|
|
841
|
-
|
|
842
|
-
log(`${LogTypes.ACK} | TransactionId: ${transactionId} | Status: dead_letter | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'} | Error: ${error}`);
|
|
843
|
-
|
|
844
|
-
return { status: 'dead_letter', transaction_id: transactionId };
|
|
845
|
-
} else {
|
|
846
|
-
// Mark as failed and increment retry count
|
|
847
|
-
const updateParams = consumerGroup
|
|
848
|
-
? [transactionId, error, consumerGroup]
|
|
849
|
-
: [transactionId, error];
|
|
850
|
-
|
|
851
|
-
const updateQuery = consumerGroup
|
|
852
|
-
? `UPDATE queen.messages_status ms
|
|
853
|
-
SET status = 'failed',
|
|
854
|
-
failed_at = NOW(),
|
|
855
|
-
error_message = $2,
|
|
856
|
-
lease_expires_at = NULL,
|
|
857
|
-
retry_count = COALESCE(retry_count, 0) + 1
|
|
858
|
-
FROM queen.messages m
|
|
859
|
-
WHERE ms.message_id = m.id
|
|
860
|
-
AND m.transaction_id = $1
|
|
861
|
-
AND ms.consumer_group = $3`
|
|
862
|
-
: `UPDATE queen.messages_status ms
|
|
863
|
-
SET status = 'failed',
|
|
864
|
-
failed_at = NOW(),
|
|
865
|
-
error_message = $2,
|
|
866
|
-
lease_expires_at = NULL,
|
|
867
|
-
retry_count = COALESCE(retry_count, 0) + 1
|
|
868
|
-
FROM queen.messages m
|
|
869
|
-
WHERE ms.message_id = m.id
|
|
870
|
-
AND m.transaction_id = $1
|
|
871
|
-
AND ms.consumer_group = '__QUEUE_MODE__'`;
|
|
872
|
-
|
|
873
|
-
await client.query(updateQuery, updateParams);
|
|
874
|
-
|
|
875
|
-
// Release partition lease on failure as well (so partition can be retried)
|
|
876
|
-
const releaseOnFailQuery = `
|
|
877
|
-
WITH message_partition AS (
|
|
878
|
-
SELECT DISTINCT m.partition_id
|
|
879
|
-
FROM queen.messages m
|
|
880
|
-
WHERE m.transaction_id = $1
|
|
881
|
-
),
|
|
882
|
-
check_remaining AS (
|
|
883
|
-
SELECT
|
|
884
|
-
mp.partition_id,
|
|
885
|
-
COUNT(ms.message_id) as processing_count
|
|
886
|
-
FROM message_partition mp
|
|
887
|
-
LEFT JOIN queen.messages m ON m.partition_id = mp.partition_id
|
|
888
|
-
LEFT JOIN queen.messages_status ms ON ms.message_id = m.id
|
|
889
|
-
AND ms.consumer_group = $2
|
|
890
|
-
AND ms.status = 'processing'
|
|
891
|
-
GROUP BY mp.partition_id
|
|
892
|
-
)
|
|
893
|
-
UPDATE queen.partition_leases pl
|
|
894
|
-
SET released_at = NOW()
|
|
895
|
-
FROM check_remaining cr
|
|
896
|
-
WHERE pl.partition_id = cr.partition_id
|
|
897
|
-
AND pl.consumer_group = $2
|
|
898
|
-
AND pl.released_at IS NULL
|
|
899
|
-
AND cr.processing_count = 0`;
|
|
900
|
-
|
|
901
|
-
await client.query(releaseOnFailQuery, [transactionId, actualConsumerGroup]);
|
|
902
|
-
|
|
903
|
-
log(`${LogTypes.ACK} | TransactionId: ${transactionId} | Status: failed (retry ${nextRetryCount}/${retryLimit}) | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'} | Error: ${error}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return { status, transaction_id: transactionId };
|
|
908
|
-
});
|
|
462
|
+
const batchResult = await acknowledgeMessages([{ transactionId, status, error }], consumerGroup);
|
|
463
|
+
return batchResult[0] || { status: 'not_found', transaction_id: transactionId };
|
|
909
464
|
};
|
|
910
465
|
|
|
911
|
-
// Batch acknowledge messages
|
|
466
|
+
// Batch acknowledge messages with cursor-based advancement
|
|
912
467
|
const acknowledgeMessages = async (acknowledgments, consumerGroup = null) => {
|
|
468
|
+
const ackStartTime = Date.now();
|
|
913
469
|
const results = [];
|
|
470
|
+
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
914
471
|
|
|
915
472
|
// Group by status for efficient processing
|
|
916
473
|
const grouped = {
|
|
@@ -924,113 +481,190 @@ const popMessages = async (scope, options = {}) => {
|
|
|
924
481
|
}
|
|
925
482
|
});
|
|
926
483
|
|
|
484
|
+
const totalMessages = acknowledgments.length;
|
|
485
|
+
const failedCount = grouped.failed.length;
|
|
486
|
+
const successCount = grouped.completed.length;
|
|
487
|
+
|
|
488
|
+
// Determine if this is a TOTAL batch failure or partial failure
|
|
489
|
+
// Only treat as "total batch failure" if it's a real batch (>1 message)
|
|
490
|
+
// Single-message failures always go to DLQ
|
|
491
|
+
const isTotalBatchFailure = failedCount === totalMessages && totalMessages > 1;
|
|
492
|
+
|
|
927
493
|
await withTransaction(pool, async (client) => {
|
|
928
|
-
//
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
WHERE ms.message_id = m.id
|
|
938
|
-
AND m.transaction_id = ANY($1::varchar[])
|
|
939
|
-
AND ms.consumer_group = $2`
|
|
940
|
-
: `UPDATE queen.messages_status ms
|
|
941
|
-
SET status = 'completed', completed_at = NOW()
|
|
942
|
-
FROM queen.messages m
|
|
943
|
-
WHERE ms.message_id = m.id
|
|
944
|
-
AND m.transaction_id = ANY($1::varchar[])
|
|
945
|
-
AND ms.consumer_group = '__QUEUE_MODE__'`;
|
|
494
|
+
// CURSOR-BASED APPROACH: Handle total batch failure vs partial failure differently
|
|
495
|
+
|
|
496
|
+
if (isTotalBatchFailure) {
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════
|
|
498
|
+
// TOTAL BATCH FAILURE: Don't advance cursor, allow retry
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════
|
|
500
|
+
log(`[CURSOR] Total batch failure - cursor NOT advanced, same batch will retry on next POP`);
|
|
501
|
+
const ackTime = Date.now() - ackStartTime;
|
|
502
|
+
const throughput = ackTime > 0 ? (totalMessages / (ackTime / 1000)).toFixed(0) : 0;
|
|
946
503
|
|
|
947
|
-
|
|
948
|
-
await client.query(updateQuery, params);
|
|
504
|
+
log(`[${LogTypes.ACK_BATCH}] Total batch failure: ${failedCount} msgs | ${throughput} msg/s | ${ackTime}ms | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
949
505
|
|
|
950
|
-
//
|
|
951
|
-
const
|
|
506
|
+
// Just release the lease - next POP will get same batch
|
|
507
|
+
const allIds = acknowledgments.map(a => a.transactionId);
|
|
508
|
+
await client.query(`
|
|
952
509
|
WITH affected_partitions AS (
|
|
953
510
|
SELECT DISTINCT m.partition_id
|
|
954
511
|
FROM queen.messages m
|
|
955
512
|
WHERE m.transaction_id = ANY($1::varchar[])
|
|
956
513
|
)
|
|
957
|
-
UPDATE queen.
|
|
958
|
-
SET
|
|
514
|
+
UPDATE queen.partition_consumers pc
|
|
515
|
+
SET lease_expires_at = NULL,
|
|
516
|
+
lease_acquired_at = NULL,
|
|
517
|
+
message_batch = NULL,
|
|
518
|
+
batch_size = 0,
|
|
519
|
+
acked_count = 0
|
|
959
520
|
FROM affected_partitions ap
|
|
960
|
-
WHERE
|
|
961
|
-
AND
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
521
|
+
WHERE pc.partition_id = ap.partition_id
|
|
522
|
+
AND pc.consumer_group = $2
|
|
523
|
+
`, [allIds, actualConsumerGroup]);
|
|
524
|
+
|
|
525
|
+
grouped.failed.forEach(ack => {
|
|
526
|
+
results.push({ transactionId: ack.transactionId, status: 'failed_retry' });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
} else {
|
|
530
|
+
// ═══════════════════════════════════════════════════════════════
|
|
531
|
+
// PARTIAL SUCCESS: Advance cursor, move failed to DLQ
|
|
532
|
+
// ═══════════════════════════════════════════════════════════════
|
|
533
|
+
|
|
534
|
+
// Get all messages in this batch (successful and failed) to find cursor position
|
|
535
|
+
const allIds = acknowledgments.map(a => a.transactionId);
|
|
536
|
+
const batchInfo = await client.query(`
|
|
537
|
+
SELECT
|
|
538
|
+
m.id,
|
|
539
|
+
m.transaction_id,
|
|
540
|
+
m.created_at,
|
|
541
|
+
m.partition_id,
|
|
542
|
+
p.name as partition_name
|
|
543
|
+
FROM queen.messages m
|
|
544
|
+
JOIN queen.partitions p ON p.id = m.partition_id
|
|
545
|
+
WHERE m.transaction_id = ANY($1::varchar[])
|
|
546
|
+
ORDER BY m.created_at DESC, m.id DESC
|
|
547
|
+
LIMIT 1
|
|
548
|
+
`, [allIds]);
|
|
549
|
+
|
|
550
|
+
if (batchInfo.rows.length === 0) {
|
|
551
|
+
throw new Error('No messages found for acknowledgment');
|
|
552
|
+
}
|
|
971
553
|
|
|
972
|
-
|
|
554
|
+
const lastMessage = batchInfo.rows[0];
|
|
555
|
+
const partitionId = lastMessage.partition_id;
|
|
556
|
+
const partitionName = lastMessage.partition_name;
|
|
973
557
|
|
|
974
|
-
|
|
558
|
+
// Advance cursor + update lease atomically in partition_consumers
|
|
559
|
+
const cursorUpdateResult = await client.query(`
|
|
560
|
+
UPDATE queen.partition_consumers
|
|
561
|
+
SET
|
|
562
|
+
-- Advance cursor (regardless of success/failure)
|
|
563
|
+
last_consumed_created_at = $1,
|
|
564
|
+
last_consumed_id = $2,
|
|
565
|
+
total_messages_consumed = total_messages_consumed + $3,
|
|
566
|
+
total_batches_consumed = total_batches_consumed + 1,
|
|
567
|
+
last_consumed_at = NOW(),
|
|
568
|
+
|
|
569
|
+
-- Update pending estimate
|
|
570
|
+
pending_estimate = GREATEST(0, pending_estimate - $3),
|
|
571
|
+
last_stats_update = NOW(),
|
|
572
|
+
|
|
573
|
+
-- Update/release lease
|
|
574
|
+
acked_count = acked_count + $3,
|
|
575
|
+
lease_expires_at = CASE
|
|
576
|
+
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
577
|
+
THEN NULL
|
|
578
|
+
ELSE lease_expires_at
|
|
579
|
+
END,
|
|
580
|
+
lease_acquired_at = CASE
|
|
581
|
+
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
582
|
+
THEN NULL
|
|
583
|
+
ELSE lease_acquired_at
|
|
584
|
+
END,
|
|
585
|
+
message_batch = CASE
|
|
586
|
+
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
587
|
+
THEN NULL
|
|
588
|
+
ELSE message_batch
|
|
589
|
+
END,
|
|
590
|
+
batch_size = CASE
|
|
591
|
+
WHEN acked_count + $3 >= batch_size AND batch_size > 0
|
|
592
|
+
THEN 0
|
|
593
|
+
ELSE batch_size
|
|
594
|
+
END
|
|
595
|
+
WHERE partition_id = $4
|
|
596
|
+
AND consumer_group = $5
|
|
597
|
+
RETURNING total_messages_consumed, total_batches_consumed,
|
|
598
|
+
(lease_expires_at IS NULL) as lease_released
|
|
599
|
+
`, [lastMessage.created_at, lastMessage.id, totalMessages, partitionId, actualConsumerGroup]);
|
|
975
600
|
|
|
601
|
+
const newTotalConsumed = cursorUpdateResult.rows[0]?.total_messages_consumed || 0;
|
|
602
|
+
const newTotalBatches = cursorUpdateResult.rows[0]?.total_batches_consumed || 0;
|
|
603
|
+
const leaseReleased = cursorUpdateResult.rows[0]?.lease_released || false;
|
|
604
|
+
|
|
605
|
+
const ackTime = Date.now() - ackStartTime;
|
|
606
|
+
const throughput = ackTime > 0 ? (totalMessages / (ackTime / 1000)).toFixed(0) : 0;
|
|
607
|
+
|
|
608
|
+
log(`[${LogTypes.ACK_BATCH}] Success: ${successCount} | Failed: ${failedCount} | ${throughput} msg/s | ${ackTime}ms | Cursor: ${newTotalConsumed} msgs | Lease: ${leaseReleased ? 'Released' : 'Active'} | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
609
|
+
|
|
610
|
+
// Move failed messages to DLQ (individual failures, not batch retry)
|
|
611
|
+
if (grouped.failed.length > 0) {
|
|
612
|
+
const failedIds = grouped.failed.map(a => a.transactionId);
|
|
613
|
+
const failedErrors = grouped.failed.map(a => a.error || 'Unknown error');
|
|
614
|
+
|
|
615
|
+
await client.query(`
|
|
616
|
+
INSERT INTO queen.dead_letter_queue (message_id, partition_id, consumer_group, error_message, original_created_at)
|
|
617
|
+
SELECT
|
|
618
|
+
m.id,
|
|
619
|
+
m.partition_id,
|
|
620
|
+
$2,
|
|
621
|
+
e.error_message,
|
|
622
|
+
m.created_at
|
|
623
|
+
FROM queen.messages m
|
|
624
|
+
CROSS JOIN LATERAL UNNEST($1::varchar[], $3::text[]) AS e(txn_id, error_message)
|
|
625
|
+
WHERE m.transaction_id = e.txn_id
|
|
626
|
+
AND m.transaction_id = ANY($1::varchar[])
|
|
627
|
+
ON CONFLICT DO NOTHING
|
|
628
|
+
`, [failedIds, actualConsumerGroup, failedErrors]);
|
|
629
|
+
|
|
630
|
+
log(`[${LogTypes.ACK_BATCH}] Moved ${failedCount} failed messages to DLQ | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Track consumption metrics for time-series analytics
|
|
634
|
+
await client.query(`
|
|
635
|
+
INSERT INTO queen.messages_consumed (partition_id, consumer_group, messages_completed, messages_failed)
|
|
636
|
+
VALUES ($1, $2, $3, $4)
|
|
637
|
+
`, [partitionId, actualConsumerGroup, successCount, failedCount]);
|
|
638
|
+
|
|
639
|
+
// Add results
|
|
976
640
|
grouped.completed.forEach(ack => {
|
|
977
641
|
results.push({ transactionId: ack.transactionId, status: 'completed' });
|
|
978
642
|
});
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const result = await acknowledgeMessage(ack.transactionId, 'failed', ack.error, consumerGroup);
|
|
984
|
-
results.push({ transactionId: ack.transactionId, ...result });
|
|
643
|
+
|
|
644
|
+
grouped.failed.forEach(ack => {
|
|
645
|
+
results.push({ transactionId: ack.transactionId, status: 'failed_dlq' });
|
|
646
|
+
});
|
|
985
647
|
}
|
|
986
648
|
});
|
|
987
649
|
|
|
988
650
|
return results;
|
|
989
651
|
};
|
|
990
652
|
|
|
991
|
-
// Reclaim expired leases
|
|
653
|
+
// Reclaim expired leases from partition_consumers
|
|
992
654
|
const reclaimExpiredLeases = async () => {
|
|
993
655
|
return withTransaction(pool, async (client) => {
|
|
994
|
-
//
|
|
995
|
-
const partitionLeaseResult = await client.query(
|
|
996
|
-
`WITH expired_leases AS (
|
|
997
|
-
UPDATE queen.partition_leases
|
|
998
|
-
SET released_at = NOW()
|
|
999
|
-
WHERE lease_expires_at < NOW()
|
|
1000
|
-
AND released_at IS NULL
|
|
1001
|
-
RETURNING partition_id, consumer_group, message_batch
|
|
1002
|
-
)
|
|
1003
|
-
-- Reset message status for messages in expired leases
|
|
1004
|
-
UPDATE queen.messages_status ms
|
|
1005
|
-
SET status = 'pending',
|
|
1006
|
-
retry_count = COALESCE(retry_count, 0) + 1,
|
|
1007
|
-
failed_at = NOW(),
|
|
1008
|
-
error_message = 'Partition lease expired',
|
|
1009
|
-
lease_expires_at = NULL,
|
|
1010
|
-
worker_id = NULL,
|
|
1011
|
-
processing_at = NULL
|
|
1012
|
-
FROM expired_leases el
|
|
1013
|
-
WHERE ms.message_id = ANY(
|
|
1014
|
-
SELECT jsonb_array_elements_text(el.message_batch)::uuid
|
|
1015
|
-
)
|
|
1016
|
-
AND ms.consumer_group = el.consumer_group
|
|
1017
|
-
RETURNING ms.message_id`
|
|
1018
|
-
);
|
|
1019
|
-
|
|
1020
|
-
if (partitionLeaseResult.rows.length > 0) {
|
|
1021
|
-
log(`${LogTypes.RECLAIM} | Expired partition leases reclaimed | Message count: ${partitionLeaseResult.rows.length}`);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Then handle individual message lease expiration
|
|
656
|
+
// Clear expired leases from partition_consumers
|
|
1025
657
|
const result = await client.query(
|
|
1026
|
-
`UPDATE queen.
|
|
1027
|
-
SET
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
658
|
+
`UPDATE queen.partition_consumers
|
|
659
|
+
SET lease_expires_at = NULL,
|
|
660
|
+
lease_acquired_at = NULL,
|
|
661
|
+
message_batch = NULL,
|
|
662
|
+
batch_size = 0,
|
|
663
|
+
acked_count = 0,
|
|
664
|
+
worker_id = NULL
|
|
665
|
+
WHERE lease_expires_at IS NOT NULL
|
|
1032
666
|
AND lease_expires_at < NOW()
|
|
1033
|
-
RETURNING
|
|
667
|
+
RETURNING partition_id, consumer_group`
|
|
1034
668
|
);
|
|
1035
669
|
|
|
1036
670
|
if (result.rows.length > 0) {
|
|
@@ -1041,7 +675,7 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1041
675
|
});
|
|
1042
676
|
|
|
1043
677
|
Object.entries(byGroup).forEach(([group, count]) => {
|
|
1044
|
-
log(
|
|
678
|
+
log(`${LogTypes.RECLAIM} | Reclaimed ${count} expired leases for consumer group: ${group}`);
|
|
1045
679
|
});
|
|
1046
680
|
}
|
|
1047
681
|
|
|
@@ -1049,7 +683,7 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1049
683
|
});
|
|
1050
684
|
};
|
|
1051
685
|
|
|
1052
|
-
// Get queue statistics (
|
|
686
|
+
// Get queue statistics (using partition_consumers for fast estimates)
|
|
1053
687
|
const getQueueStats = async (filters = {}) => {
|
|
1054
688
|
const { queue, namespace, task, fromDateTime, toDateTime } = filters;
|
|
1055
689
|
|
|
@@ -1059,19 +693,26 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1059
693
|
q.namespace,
|
|
1060
694
|
q.task,
|
|
1061
695
|
p.name as partition,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
COUNT(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
COUNT(
|
|
696
|
+
COALESCE(pc.pending_estimate, 0) as pending,
|
|
697
|
+
CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as processing,
|
|
698
|
+
COALESCE(pc.total_messages_consumed, 0) as completed,
|
|
699
|
+
0 as failed,
|
|
700
|
+
(SELECT COUNT(*) FROM queen.dead_letter_queue dlq
|
|
701
|
+
WHERE dlq.partition_id = p.id
|
|
702
|
+
AND dlq.consumer_group = COALESCE(pc.consumer_group, '__QUEUE_MODE__')) as dead_letter,
|
|
703
|
+
(SELECT COUNT(*) FROM queen.messages m
|
|
704
|
+
WHERE m.partition_id = p.id
|
|
705
|
+
${fromDateTime ? 'AND m.created_at >= $' + (1 + (queue ? 1 : 0) + (namespace ? 1 : 0) + (task ? 1 : 0)) + '::timestamp' : ''}
|
|
706
|
+
${toDateTime ? 'AND m.created_at <= $' + (1 + (queue ? 1 : 0) + (namespace ? 1 : 0) + (task ? 1 : 0) + (fromDateTime ? 1 : 0)) + '::timestamp' : ''}
|
|
707
|
+
) as total_messages,
|
|
708
|
+
(SELECT COUNT(DISTINCT consumer_group)
|
|
709
|
+
FROM queen.partition_consumers
|
|
710
|
+
WHERE partition_id = p.id
|
|
711
|
+
AND consumer_group != '__QUEUE_MODE__') as consumer_groups_count
|
|
1070
712
|
FROM queen.queues q
|
|
1071
713
|
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
1072
|
-
LEFT JOIN queen.
|
|
1073
|
-
|
|
1074
|
-
LEFT JOIN queen.consumer_groups cg ON cg.queue_id = q.id AND cg.active = true
|
|
714
|
+
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
715
|
+
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
1075
716
|
`;
|
|
1076
717
|
|
|
1077
718
|
const conditions = [];
|
|
@@ -1092,14 +733,12 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1092
733
|
params.push(task);
|
|
1093
734
|
}
|
|
1094
735
|
|
|
1095
|
-
// Add date
|
|
736
|
+
// Add date params for subqueries
|
|
1096
737
|
if (fromDateTime) {
|
|
1097
|
-
conditions.push(`m.created_at >= $${params.length + 1}::timestamp`);
|
|
1098
738
|
params.push(fromDateTime);
|
|
1099
739
|
}
|
|
1100
740
|
|
|
1101
741
|
if (toDateTime) {
|
|
1102
|
-
conditions.push(`m.created_at <= $${params.length + 1}::timestamp`);
|
|
1103
742
|
params.push(toDateTime);
|
|
1104
743
|
}
|
|
1105
744
|
|
|
@@ -1107,14 +746,13 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1107
746
|
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
1108
747
|
}
|
|
1109
748
|
|
|
1110
|
-
query += `
|
|
1111
|
-
ORDER BY q.name, p.name, cg.name`;
|
|
749
|
+
query += ` ORDER BY q.name, p.name`;
|
|
1112
750
|
|
|
1113
751
|
const result = await pool.query(query, params);
|
|
1114
752
|
return result.rows;
|
|
1115
753
|
};
|
|
1116
754
|
|
|
1117
|
-
// Get queue lag statistics
|
|
755
|
+
// Get queue lag statistics (simplified with partition_consumers)
|
|
1118
756
|
const getQueueLag = async (filters = {}) => {
|
|
1119
757
|
const { queue, namespace, task, fromDateTime, toDateTime } = filters;
|
|
1120
758
|
|
|
@@ -1125,49 +763,32 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1125
763
|
q.namespace,
|
|
1126
764
|
q.task,
|
|
1127
765
|
p.name as partition,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
THEN EXTRACT(EPOCH FROM (ms.completed_at - m.created_at))
|
|
1146
|
-
ELSE NULL
|
|
1147
|
-
END
|
|
1148
|
-
) as median_processing_time_seconds,
|
|
1149
|
-
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY
|
|
1150
|
-
CASE
|
|
1151
|
-
WHEN ms.status = 'completed' AND ms.completed_at IS NOT NULL AND m.created_at IS NOT NULL
|
|
1152
|
-
THEN EXTRACT(EPOCH FROM (ms.completed_at - m.created_at))
|
|
1153
|
-
ELSE NULL
|
|
1154
|
-
END
|
|
1155
|
-
) as p95_processing_time_seconds,
|
|
1156
|
-
MIN(m.created_at) FILTER (WHERE ms.status IN ('pending', 'processing')) as oldest_unprocessed,
|
|
1157
|
-
MAX(m.created_at) FILTER (WHERE ms.status = 'completed') as newest_completed
|
|
766
|
+
COALESCE(pc.pending_estimate, 0) as pending_count,
|
|
767
|
+
CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as processing_count,
|
|
768
|
+
COALESCE(pc.pending_estimate, 0) + CASE WHEN pc.lease_expires_at IS NOT NULL THEN 1 ELSE 0 END as total_backlog,
|
|
769
|
+
COALESCE(pc.total_messages_consumed, 0) as completed_messages,
|
|
770
|
+
0 as avg_processing_time_seconds,
|
|
771
|
+
0 as median_processing_time_seconds,
|
|
772
|
+
0 as p95_processing_time_seconds,
|
|
773
|
+
(SELECT MIN(created_at) FROM queen.messages m
|
|
774
|
+
WHERE m.partition_id = p.id
|
|
775
|
+
AND (m.created_at, m.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
|
|
776
|
+
COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
777
|
+
) as oldest_unprocessed,
|
|
778
|
+
(SELECT MAX(created_at) FROM queen.messages m
|
|
779
|
+
WHERE m.partition_id = p.id
|
|
780
|
+
AND (m.created_at, m.id) <= (COALESCE(pc.last_consumed_created_at, NOW()::timestamptz),
|
|
781
|
+
COALESCE(pc.last_consumed_id, gen_random_uuid()))
|
|
782
|
+
) as newest_completed
|
|
1158
783
|
FROM queen.queues q
|
|
1159
784
|
LEFT JOIN queen.partitions p ON p.queue_id = q.id
|
|
1160
|
-
LEFT JOIN queen.
|
|
1161
|
-
|
|
785
|
+
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
|
|
786
|
+
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
1162
787
|
`;
|
|
1163
788
|
|
|
1164
789
|
const conditions = [];
|
|
1165
790
|
const params = [];
|
|
1166
791
|
|
|
1167
|
-
// Add date params first (they're referenced in the query above as $1 and $2)
|
|
1168
|
-
params.push(fromDateTime || null);
|
|
1169
|
-
params.push(toDateTime || null);
|
|
1170
|
-
|
|
1171
792
|
if (queue) {
|
|
1172
793
|
conditions.push(`q.name = $${params.length + 1}`);
|
|
1173
794
|
params.push(queue);
|
|
@@ -1187,7 +808,9 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1187
808
|
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
1188
809
|
}
|
|
1189
810
|
|
|
1190
|
-
query += ` GROUP BY q.name, q.namespace, q.task, p.name
|
|
811
|
+
query += ` GROUP BY q.name, q.namespace, q.task, p.name, pc.pending_estimate,
|
|
812
|
+
pc.lease_expires_at, pc.total_messages_consumed,
|
|
813
|
+
pc.last_consumed_created_at, pc.last_consumed_id, p.id
|
|
1191
814
|
)
|
|
1192
815
|
SELECT
|
|
1193
816
|
queue,
|
|
@@ -1199,9 +822,9 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1199
822
|
'processingCount', processing_count,
|
|
1200
823
|
'totalBacklog', total_backlog,
|
|
1201
824
|
'completedMessages', completed_messages,
|
|
1202
|
-
'avgProcessingTimeSeconds',
|
|
1203
|
-
'medianProcessingTimeSeconds',
|
|
1204
|
-
'p95ProcessingTimeSeconds',
|
|
825
|
+
'avgProcessingTimeSeconds', avg_processing_time_seconds,
|
|
826
|
+
'medianProcessingTimeSeconds', median_processing_time_seconds,
|
|
827
|
+
'p95ProcessingTimeSeconds', p95_processing_time_seconds,
|
|
1205
828
|
'estimatedLagSeconds', CASE
|
|
1206
829
|
WHEN oldest_unprocessed IS NOT NULL
|
|
1207
830
|
THEN EXTRACT(EPOCH FROM (NOW() - oldest_unprocessed))
|
|
@@ -1225,165 +848,6 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1225
848
|
return result.rows;
|
|
1226
849
|
};
|
|
1227
850
|
|
|
1228
|
-
/**
|
|
1229
|
-
* Build query for popping messages with filters (namespace/task)
|
|
1230
|
-
* This is a helper function to construct the complex query used in popMessagesWithFilters
|
|
1231
|
-
*/
|
|
1232
|
-
const buildPopMessagesWithFiltersQuery = (actualConsumerGroup, namespace, task, batch) => {
|
|
1233
|
-
let query = `
|
|
1234
|
-
WITH available_messages AS (
|
|
1235
|
-
SELECT m.id, m.transaction_id, m.trace_id, m.payload, m.is_encrypted, m.created_at,
|
|
1236
|
-
p.id as partition_id, p.name as partition_name, q.name as queue_name,
|
|
1237
|
-
q.priority, q.lease_time, q.retry_limit, q.delayed_processing,
|
|
1238
|
-
q.window_buffer, q.max_wait_time_seconds, q.namespace, q.task
|
|
1239
|
-
FROM queen.messages m
|
|
1240
|
-
JOIN queen.partitions p ON m.partition_id = p.id
|
|
1241
|
-
JOIN queen.queues q ON p.queue_id = q.id
|
|
1242
|
-
LEFT JOIN queen.messages_status ms ON m.id = ms.message_id AND ms.consumer_group = $1
|
|
1243
|
-
WHERE (ms.id IS NULL OR ms.status = 'pending')
|
|
1244
|
-
AND m.created_at <= NOW() - INTERVAL '1 second' * q.delayed_processing
|
|
1245
|
-
AND (q.max_wait_time_seconds = 0 OR
|
|
1246
|
-
m.created_at > NOW() - INTERVAL '1 second' * q.max_wait_time_seconds)
|
|
1247
|
-
AND (q.window_buffer = 0 OR NOT EXISTS (
|
|
1248
|
-
SELECT 1 FROM queen.messages m2
|
|
1249
|
-
WHERE m2.partition_id = p.id
|
|
1250
|
-
AND m2.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
1251
|
-
))
|
|
1252
|
-
AND NOT EXISTS (
|
|
1253
|
-
-- Check for active partition leases
|
|
1254
|
-
SELECT 1 FROM queen.partition_leases pl
|
|
1255
|
-
WHERE pl.partition_id = p.id
|
|
1256
|
-
AND pl.consumer_group = $1
|
|
1257
|
-
AND pl.released_at IS NULL
|
|
1258
|
-
AND pl.lease_expires_at > NOW()
|
|
1259
|
-
)
|
|
1260
|
-
`;
|
|
1261
|
-
|
|
1262
|
-
const params = [actualConsumerGroup];
|
|
1263
|
-
|
|
1264
|
-
if (namespace) {
|
|
1265
|
-
params.push(namespace);
|
|
1266
|
-
query += ` AND q.namespace = $${params.length}`;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (task) {
|
|
1270
|
-
params.push(task);
|
|
1271
|
-
query += ` AND q.task = $${params.length}`;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
params.push(batch);
|
|
1275
|
-
query += ` ORDER BY q.priority DESC, m.created_at ASC LIMIT $${params.length} FOR UPDATE OF m SKIP LOCKED`;
|
|
1276
|
-
|
|
1277
|
-
// Add the INSERT part
|
|
1278
|
-
params.push(config.WORKER_ID);
|
|
1279
|
-
query += `)
|
|
1280
|
-
INSERT INTO queen.messages_status (message_id, consumer_group, status, lease_expires_at, processing_at, worker_id)
|
|
1281
|
-
SELECT id, $1, 'processing', NOW() + INTERVAL '1 second' * lease_time, NOW(), $${params.length}
|
|
1282
|
-
FROM available_messages
|
|
1283
|
-
ON CONFLICT (message_id, consumer_group)
|
|
1284
|
-
DO UPDATE SET
|
|
1285
|
-
status = 'processing',
|
|
1286
|
-
lease_expires_at = EXCLUDED.lease_expires_at,
|
|
1287
|
-
processing_at = EXCLUDED.processing_at,
|
|
1288
|
-
worker_id = EXCLUDED.worker_id,
|
|
1289
|
-
retry_count = queen.messages_status.retry_count + 1
|
|
1290
|
-
RETURNING message_id,
|
|
1291
|
-
(SELECT transaction_id FROM queen.messages WHERE id = message_id),
|
|
1292
|
-
(SELECT trace_id FROM queen.messages WHERE id = message_id),
|
|
1293
|
-
(SELECT payload FROM queen.messages WHERE id = message_id),
|
|
1294
|
-
(SELECT is_encrypted FROM queen.messages WHERE id = message_id),
|
|
1295
|
-
(SELECT created_at FROM queen.messages WHERE id = message_id),
|
|
1296
|
-
(SELECT p.id FROM queen.messages m2 JOIN queen.partitions p ON m2.partition_id = p.id WHERE m2.id = message_id) as partition_id,
|
|
1297
|
-
(SELECT p.name FROM queen.messages m2 JOIN queen.partitions p ON m2.partition_id = p.id WHERE m2.id = message_id) as partition_name,
|
|
1298
|
-
(SELECT q.name FROM queen.messages m2 JOIN queen.partitions p ON m2.partition_id = p.id JOIN queen.queues q ON p.queue_id = q.id WHERE m2.id = message_id) as queue_name,
|
|
1299
|
-
(SELECT q.priority FROM queen.messages m2 JOIN queen.partitions p ON m2.partition_id = p.id JOIN queen.queues q ON p.queue_id = q.id WHERE m2.id = message_id) as priority,
|
|
1300
|
-
(SELECT q.lease_time FROM queen.messages m2 JOIN queen.partitions p ON m2.partition_id = p.id JOIN queen.queues q ON p.queue_id = q.id WHERE m2.id = message_id) as lease_time,
|
|
1301
|
-
retry_count
|
|
1302
|
-
`;
|
|
1303
|
-
|
|
1304
|
-
return { query, params };
|
|
1305
|
-
};
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Pop messages with namespace/task filtering
|
|
1309
|
-
* Supports filtering by namespace and/or task in addition to consumer groups
|
|
1310
|
-
*/
|
|
1311
|
-
const popMessagesWithFilters = async (filters, options = {}) => {
|
|
1312
|
-
const { namespace, task, consumerGroup } = filters;
|
|
1313
|
-
const { wait = false, timeout = config.QUEUE.DEFAULT_TIMEOUT, batch = config.QUEUE.DEFAULT_BATCH_SIZE } = options;
|
|
1314
|
-
|
|
1315
|
-
return withTransaction(pool, async (client) => {
|
|
1316
|
-
// Determine the actual consumer group to use
|
|
1317
|
-
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
1318
|
-
|
|
1319
|
-
// Use the helper function to build the query
|
|
1320
|
-
const { query, params } = buildPopMessagesWithFiltersQuery(actualConsumerGroup, namespace, task, batch);
|
|
1321
|
-
|
|
1322
|
-
const result = await client.query(query, params);
|
|
1323
|
-
|
|
1324
|
-
// Lock all partitions that we got messages from
|
|
1325
|
-
if (result.rows.length > 0) {
|
|
1326
|
-
const partitionIds = [...new Set(result.rows.map(r => r.partition_id))];
|
|
1327
|
-
const leaseTime = Math.max(...result.rows.map(r => r.lease_time || 30));
|
|
1328
|
-
|
|
1329
|
-
// Acquire partition leases for all partitions we got messages from
|
|
1330
|
-
for (const partitionId of partitionIds) {
|
|
1331
|
-
await client.query(`
|
|
1332
|
-
INSERT INTO queen.partition_leases (
|
|
1333
|
-
partition_id,
|
|
1334
|
-
consumer_group,
|
|
1335
|
-
lease_expires_at
|
|
1336
|
-
) VALUES ($1, $2, NOW() + INTERVAL '1 second' * $3)
|
|
1337
|
-
ON CONFLICT (partition_id, consumer_group)
|
|
1338
|
-
DO UPDATE SET
|
|
1339
|
-
lease_expires_at = EXCLUDED.lease_expires_at,
|
|
1340
|
-
released_at = NULL
|
|
1341
|
-
`, [partitionId, actualConsumerGroup, leaseTime]);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
if (result.rows.length === 0 && wait) {
|
|
1346
|
-
// For namespace/task filtering, we can't easily wait on specific queue paths
|
|
1347
|
-
// So we'll use simple polling
|
|
1348
|
-
return { messages: [] };
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Format and decrypt messages
|
|
1352
|
-
const messages = await Promise.all(
|
|
1353
|
-
result.rows.map(async (row) => {
|
|
1354
|
-
let decryptedPayload = row.payload;
|
|
1355
|
-
|
|
1356
|
-
if (row.is_encrypted && encryption.isEncryptionEnabled()) {
|
|
1357
|
-
try {
|
|
1358
|
-
decryptedPayload = await encryption.decryptPayload(row.payload);
|
|
1359
|
-
} catch (error) {
|
|
1360
|
-
log('ERROR: Decryption failed:', error);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
return {
|
|
1365
|
-
id: row.message_id,
|
|
1366
|
-
transactionId: row.transaction_id,
|
|
1367
|
-
traceId: row.trace_id,
|
|
1368
|
-
queue: row.queue_name,
|
|
1369
|
-
partition: row.partition_name,
|
|
1370
|
-
data: decryptedPayload,
|
|
1371
|
-
payload: decryptedPayload,
|
|
1372
|
-
retryCount: row.retry_count || 0,
|
|
1373
|
-
priority: row.priority || 0,
|
|
1374
|
-
createdAt: row.created_at
|
|
1375
|
-
};
|
|
1376
|
-
})
|
|
1377
|
-
);
|
|
1378
|
-
|
|
1379
|
-
if (messages.length > 0) {
|
|
1380
|
-
log(`${LogTypes.POP} | Count: ${messages.length} | Namespace: ${namespace || 'ANY'} | Task: ${task || 'ANY'}`);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
return { messages };
|
|
1384
|
-
});
|
|
1385
|
-
};
|
|
1386
|
-
|
|
1387
851
|
/**
|
|
1388
852
|
* Configure queue settings with namespace and task support
|
|
1389
853
|
* Updates queue configuration options and optionally sets namespace/task
|
|
@@ -1495,18 +959,444 @@ const popMessages = async (scope, options = {}) => {
|
|
|
1495
959
|
});
|
|
1496
960
|
};
|
|
1497
961
|
|
|
962
|
+
/**
|
|
963
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
964
|
+
* UNIFIED POP FUNCTION - Handles all 3 access patterns in one implementation
|
|
965
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
966
|
+
*
|
|
967
|
+
* This function unifies:
|
|
968
|
+
* 1. Direct partition access (queue + partition specified)
|
|
969
|
+
* 2. Queue-level access (queue only, find unlocked partition)
|
|
970
|
+
* 3. Filtered access (namespace/task, find matching queues + partitions)
|
|
971
|
+
*
|
|
972
|
+
* Benefits over separate functions:
|
|
973
|
+
* - Single source of truth for lease acquisition
|
|
974
|
+
* - Consistent multi-candidate retry logic
|
|
975
|
+
* - Easier to maintain and optimize
|
|
976
|
+
* - Shared backpressure and metrics
|
|
977
|
+
*/
|
|
978
|
+
const uniquePop = async (scope, options = {}) => {
|
|
979
|
+
const {
|
|
980
|
+
queue,
|
|
981
|
+
partition,
|
|
982
|
+
namespace,
|
|
983
|
+
task,
|
|
984
|
+
consumerGroup
|
|
985
|
+
} = scope;
|
|
986
|
+
|
|
987
|
+
const {
|
|
988
|
+
wait = false,
|
|
989
|
+
timeout = config.QUEUE.DEFAULT_TIMEOUT,
|
|
990
|
+
batch = config.QUEUE.DEFAULT_BATCH_SIZE,
|
|
991
|
+
subscriptionMode = null,
|
|
992
|
+
subscriptionFrom = null
|
|
993
|
+
} = options;
|
|
994
|
+
|
|
995
|
+
const startTime = Date.now();
|
|
996
|
+
const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
|
|
997
|
+
|
|
998
|
+
// Determine access mode
|
|
999
|
+
const accessMode = partition ? 'direct' : (namespace || task ? 'filtered' : 'queue');
|
|
1000
|
+
|
|
1001
|
+
return withTransaction(pool, async (client) => {
|
|
1002
|
+
const connectionWaitTime = Date.now() - startTime;
|
|
1003
|
+
if (connectionWaitTime > 50) {
|
|
1004
|
+
log(`⚠️ : ${connectionWaitTime}ms`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1008
|
+
// PHASE 1: Find Candidate Partitions (mode-specific queries)
|
|
1009
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1010
|
+
|
|
1011
|
+
const candidateQueryStart = Date.now();
|
|
1012
|
+
let candidatePartitions = [];
|
|
1013
|
+
let queueInfo = null;
|
|
1014
|
+
|
|
1015
|
+
if (accessMode === 'direct') {
|
|
1016
|
+
// CASE 1: Direct partition access - single partition, no search needed
|
|
1017
|
+
const result = await client.query(`
|
|
1018
|
+
SELECT
|
|
1019
|
+
q.id as queue_id,
|
|
1020
|
+
q.name as queue_name,
|
|
1021
|
+
q.lease_time,
|
|
1022
|
+
q.retry_limit,
|
|
1023
|
+
q.delayed_processing,
|
|
1024
|
+
q.max_wait_time_seconds,
|
|
1025
|
+
q.window_buffer,
|
|
1026
|
+
q.priority,
|
|
1027
|
+
p.id as partition_id,
|
|
1028
|
+
p.name as partition_name
|
|
1029
|
+
FROM queen.queues q
|
|
1030
|
+
JOIN queen.partitions p ON p.queue_id = q.id
|
|
1031
|
+
WHERE q.name = $1 AND p.name = $2
|
|
1032
|
+
`, [queue, partition]);
|
|
1033
|
+
|
|
1034
|
+
if (result.rows.length > 0) {
|
|
1035
|
+
candidatePartitions = [result.rows[0]];
|
|
1036
|
+
queueInfo = result.rows[0];
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
} else if (accessMode === 'queue') {
|
|
1040
|
+
// CASE 2: Queue-level access - find unlocked partitions
|
|
1041
|
+
const result = await client.query(`
|
|
1042
|
+
SELECT
|
|
1043
|
+
q.id as queue_id,
|
|
1044
|
+
q.name as queue_name,
|
|
1045
|
+
q.lease_time,
|
|
1046
|
+
q.retry_limit,
|
|
1047
|
+
q.delayed_processing,
|
|
1048
|
+
q.max_wait_time_seconds,
|
|
1049
|
+
q.window_buffer,
|
|
1050
|
+
q.priority,
|
|
1051
|
+
p.id as partition_id,
|
|
1052
|
+
p.name as partition_name
|
|
1053
|
+
FROM queen.queues q
|
|
1054
|
+
JOIN queen.partitions p ON p.queue_id = q.id
|
|
1055
|
+
WHERE q.name = $2
|
|
1056
|
+
AND NOT EXISTS (
|
|
1057
|
+
SELECT 1 FROM queen.partition_consumers pc
|
|
1058
|
+
WHERE pc.partition_id = p.id
|
|
1059
|
+
AND pc.consumer_group = $1
|
|
1060
|
+
AND pc.lease_expires_at IS NOT NULL
|
|
1061
|
+
AND pc.lease_expires_at > NOW()
|
|
1062
|
+
)
|
|
1063
|
+
ORDER BY RANDOM()
|
|
1064
|
+
LIMIT $3
|
|
1065
|
+
`, [actualConsumerGroup, queue, config.QUEUE.MAX_PARTITION_CANDIDATES]);
|
|
1066
|
+
|
|
1067
|
+
if (result.rows.length > 0) {
|
|
1068
|
+
candidatePartitions = result.rows;
|
|
1069
|
+
queueInfo = result.rows[0];
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
} else {
|
|
1073
|
+
// CASE 3: Filtered access - namespace/task
|
|
1074
|
+
let filterQuery = `
|
|
1075
|
+
SELECT
|
|
1076
|
+
q.id as queue_id,
|
|
1077
|
+
q.name as queue_name,
|
|
1078
|
+
q.lease_time,
|
|
1079
|
+
q.retry_limit,
|
|
1080
|
+
q.delayed_processing,
|
|
1081
|
+
q.max_wait_time_seconds,
|
|
1082
|
+
q.window_buffer,
|
|
1083
|
+
q.priority,
|
|
1084
|
+
p.id as partition_id,
|
|
1085
|
+
p.name as partition_name,
|
|
1086
|
+
MIN(m.created_at) as oldest_message
|
|
1087
|
+
FROM queen.messages m
|
|
1088
|
+
JOIN queen.partitions p ON m.partition_id = p.id
|
|
1089
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
1090
|
+
LEFT JOIN queen.partition_consumers pc ON p.id = pc.partition_id
|
|
1091
|
+
AND pc.consumer_group = $1
|
|
1092
|
+
WHERE m.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)
|
|
1093
|
+
AND (q.delayed_processing = 0 OR m.created_at <= NOW() - INTERVAL '1 second' * q.delayed_processing)
|
|
1094
|
+
AND (q.max_wait_time_seconds = 0 OR
|
|
1095
|
+
m.created_at > NOW() - INTERVAL '1 second' * q.max_wait_time_seconds)
|
|
1096
|
+
AND (q.window_buffer = 0 OR NOT EXISTS (
|
|
1097
|
+
SELECT 1 FROM queen.messages m2
|
|
1098
|
+
WHERE m2.partition_id = p.id
|
|
1099
|
+
AND m2.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
|
|
1100
|
+
))
|
|
1101
|
+
AND NOT EXISTS (
|
|
1102
|
+
SELECT 1 FROM queen.partition_consumers pc2
|
|
1103
|
+
WHERE pc2.partition_id = p.id
|
|
1104
|
+
AND pc2.consumer_group = $1
|
|
1105
|
+
AND pc2.lease_expires_at IS NOT NULL
|
|
1106
|
+
AND pc2.lease_expires_at > NOW()
|
|
1107
|
+
)
|
|
1108
|
+
`;
|
|
1109
|
+
|
|
1110
|
+
const params = [actualConsumerGroup];
|
|
1111
|
+
|
|
1112
|
+
if (namespace) {
|
|
1113
|
+
params.push(namespace);
|
|
1114
|
+
filterQuery += ` AND q.namespace = $${params.length}`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (task) {
|
|
1118
|
+
params.push(task);
|
|
1119
|
+
filterQuery += ` AND q.task = $${params.length}`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
params.push(config.QUEUE.MAX_PARTITION_CANDIDATES);
|
|
1123
|
+
filterQuery += `
|
|
1124
|
+
GROUP BY q.id, q.name, q.lease_time, q.retry_limit, q.delayed_processing,
|
|
1125
|
+
q.max_wait_time_seconds, q.window_buffer, q.priority,
|
|
1126
|
+
p.id, p.name
|
|
1127
|
+
HAVING COUNT(*) > 0
|
|
1128
|
+
ORDER BY q.priority DESC, MIN(m.created_at) ASC, RANDOM()
|
|
1129
|
+
LIMIT $${params.length}
|
|
1130
|
+
`;
|
|
1131
|
+
|
|
1132
|
+
const result = await client.query(filterQuery, params);
|
|
1133
|
+
|
|
1134
|
+
if (result.rows.length > 0) {
|
|
1135
|
+
candidatePartitions = result.rows;
|
|
1136
|
+
queueInfo = result.rows[0];
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const candidateQueryTime = Date.now() - candidateQueryStart;
|
|
1141
|
+
|
|
1142
|
+
if (candidatePartitions.length === 0) {
|
|
1143
|
+
return { messages: [] };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1147
|
+
// PHASE 2: Try to Acquire Lease on a Candidate Partition (shared logic)
|
|
1148
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1149
|
+
|
|
1150
|
+
let acquiredPartition = null;
|
|
1151
|
+
const MAX_ATTEMPTS = accessMode === 'direct' ? 1 : 3;
|
|
1152
|
+
|
|
1153
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1154
|
+
// Shuffle candidates to distribute across consumers
|
|
1155
|
+
const shuffled = [...candidatePartitions];
|
|
1156
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
1157
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1158
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Try to acquire lease on each candidate
|
|
1162
|
+
for (const candidate of shuffled) {
|
|
1163
|
+
const leaseAcqStart = Date.now();
|
|
1164
|
+
|
|
1165
|
+
// Try to acquire lease - check if lease is available based on DB state
|
|
1166
|
+
// The key insight: released_at IS NULL means someone holds the lease
|
|
1167
|
+
// We can only acquire if: released_at IS NOT NULL OR lease expired
|
|
1168
|
+
try {
|
|
1169
|
+
// Get or create cursor position for this partition/consumer group
|
|
1170
|
+
// Handle subscription modes for bus mode (new-only, from-timestamp, etc.)
|
|
1171
|
+
let initialCursorId = '00000000-0000-0000-0000-000000000000';
|
|
1172
|
+
let initialCursorTimestamp = null;
|
|
1173
|
+
|
|
1174
|
+
// For bus mode with subscription preferences
|
|
1175
|
+
if (consumerGroup && (subscriptionMode || subscriptionFrom)) {
|
|
1176
|
+
if (subscriptionMode === 'new' || subscriptionMode === 'new-only' || subscriptionFrom === 'now') {
|
|
1177
|
+
// Start from NOW - get the latest message ID to use as cursor
|
|
1178
|
+
const latestMsg = await client.query(`
|
|
1179
|
+
SELECT id, created_at FROM queen.messages
|
|
1180
|
+
WHERE partition_id = $1
|
|
1181
|
+
ORDER BY created_at DESC, id DESC
|
|
1182
|
+
LIMIT 1
|
|
1183
|
+
`, [candidate.partition_id]);
|
|
1184
|
+
|
|
1185
|
+
if (latestMsg.rows.length > 0) {
|
|
1186
|
+
initialCursorId = latestMsg.rows[0].id;
|
|
1187
|
+
initialCursorTimestamp = latestMsg.rows[0].created_at;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// else: subscriptionFrom='all' or no preference → start from beginning (00000000...)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Step 1: Ensure cursor exists (idempotent)
|
|
1194
|
+
await client.query(`
|
|
1195
|
+
INSERT INTO queen.partition_consumers (
|
|
1196
|
+
partition_id,
|
|
1197
|
+
consumer_group,
|
|
1198
|
+
last_consumed_id,
|
|
1199
|
+
last_consumed_created_at
|
|
1200
|
+
)
|
|
1201
|
+
VALUES ($1, $2, $3, $4)
|
|
1202
|
+
ON CONFLICT (partition_id, consumer_group) DO NOTHING
|
|
1203
|
+
`, [candidate.partition_id, actualConsumerGroup, initialCursorId, initialCursorTimestamp]);
|
|
1204
|
+
|
|
1205
|
+
// Step 2: Try to acquire lease and get cursor position
|
|
1206
|
+
const leaseResult = await client.query(`
|
|
1207
|
+
UPDATE queen.partition_consumers
|
|
1208
|
+
SET lease_expires_at = NOW() + INTERVAL '1 second' * $3,
|
|
1209
|
+
lease_acquired_at = NOW(),
|
|
1210
|
+
message_batch = NULL,
|
|
1211
|
+
batch_size = 0,
|
|
1212
|
+
acked_count = 0
|
|
1213
|
+
WHERE partition_id = $1
|
|
1214
|
+
AND consumer_group = $2
|
|
1215
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= NOW())
|
|
1216
|
+
RETURNING partition_id, last_consumed_id, last_consumed_created_at,
|
|
1217
|
+
total_messages_consumed, lease_expires_at
|
|
1218
|
+
`, [candidate.partition_id, actualConsumerGroup, candidate.lease_time]);
|
|
1219
|
+
|
|
1220
|
+
// Check if we acquired the lease
|
|
1221
|
+
const acquired = leaseResult.rows.length > 0;
|
|
1222
|
+
const result = leaseResult.rows[0];
|
|
1223
|
+
|
|
1224
|
+
if (acquired) {
|
|
1225
|
+
// Lease acquired! Now check if this partition has messages
|
|
1226
|
+
// If not, release and try next candidate
|
|
1227
|
+
const messageQueryStart = Date.now();
|
|
1228
|
+
|
|
1229
|
+
const cursor = result;
|
|
1230
|
+
const cursorId = cursor.last_consumed_id || '00000000-0000-0000-0000-000000000000';
|
|
1231
|
+
const totalConsumed = cursor.total_messages_consumed || 0;
|
|
1232
|
+
|
|
1233
|
+
// Cursor-based query using ONLY UUID comparison (UUIDv7 is time-ordered)
|
|
1234
|
+
// Conditionally add timestamp filters only when configured
|
|
1235
|
+
const hasDelayedProcessing = candidate.delayed_processing > 0;
|
|
1236
|
+
const hasEviction = candidate.max_wait_time_seconds > 0;
|
|
1237
|
+
|
|
1238
|
+
let whereClause = 'm.partition_id = $4 AND m.id > $5::uuid';
|
|
1239
|
+
let queryParams = [
|
|
1240
|
+
candidate.partition_name,
|
|
1241
|
+
candidate.queue_name,
|
|
1242
|
+
candidate.priority || 0,
|
|
1243
|
+
candidate.partition_id,
|
|
1244
|
+
cursorId
|
|
1245
|
+
];
|
|
1246
|
+
let paramIndex = 6;
|
|
1247
|
+
|
|
1248
|
+
if (hasDelayedProcessing) {
|
|
1249
|
+
whereClause += ` AND m.created_at <= NOW() - INTERVAL '1 second' * $${paramIndex}`;
|
|
1250
|
+
queryParams.push(candidate.delayed_processing);
|
|
1251
|
+
paramIndex++;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (hasEviction) {
|
|
1255
|
+
whereClause += ` AND m.created_at > NOW() - INTERVAL '1 second' * $${paramIndex}`;
|
|
1256
|
+
queryParams.push(candidate.max_wait_time_seconds);
|
|
1257
|
+
paramIndex++;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
queryParams.push(batch);
|
|
1261
|
+
const limitParam = `$${paramIndex}`;
|
|
1262
|
+
|
|
1263
|
+
const messagesResult = await client.query(`
|
|
1264
|
+
SELECT
|
|
1265
|
+
m.id,
|
|
1266
|
+
m.transaction_id,
|
|
1267
|
+
m.trace_id,
|
|
1268
|
+
m.payload,
|
|
1269
|
+
m.is_encrypted,
|
|
1270
|
+
m.created_at,
|
|
1271
|
+
$1 as partition_name,
|
|
1272
|
+
$2 as queue_name,
|
|
1273
|
+
$3 as priority,
|
|
1274
|
+
0 as retry_count
|
|
1275
|
+
FROM queen.messages m
|
|
1276
|
+
WHERE ${whereClause}
|
|
1277
|
+
ORDER BY m.created_at ASC, m.id ASC
|
|
1278
|
+
LIMIT ${limitParam}
|
|
1279
|
+
FOR UPDATE OF m SKIP LOCKED
|
|
1280
|
+
`, queryParams)
|
|
1281
|
+
.catch(err => {
|
|
1282
|
+
if (err.code === '55P03') { // Lock not available
|
|
1283
|
+
return { rows: [] };
|
|
1284
|
+
}
|
|
1285
|
+
throw err;
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
const messageQueryTime = Date.now() - messageQueryStart;
|
|
1289
|
+
const messages = messagesResult.rows;
|
|
1290
|
+
|
|
1291
|
+
// Check if we found messages
|
|
1292
|
+
if (messages.length === 0) {
|
|
1293
|
+
// No messages in this partition (cursor exhausted) - release lease and try next candidate
|
|
1294
|
+
log(`[CURSOR] Partition ${candidate.partition_name} exhausted after ${totalConsumed} messages`);
|
|
1295
|
+
await client.query(`
|
|
1296
|
+
UPDATE queen.partition_consumers
|
|
1297
|
+
SET lease_expires_at = NULL,
|
|
1298
|
+
lease_acquired_at = NULL
|
|
1299
|
+
WHERE partition_id = $1 AND consumer_group = $2
|
|
1300
|
+
`, [candidate.partition_id, actualConsumerGroup]);
|
|
1301
|
+
continue; // Try next candidate partition
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Success! We have messages from this partition
|
|
1305
|
+
acquiredPartition = candidate;
|
|
1306
|
+
|
|
1307
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1308
|
+
// PHASE 4: Update Partition Lease with Message Batch
|
|
1309
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1310
|
+
|
|
1311
|
+
const leaseUpdateStart = Date.now();
|
|
1312
|
+
const messageIds = messages.map(m => m.id);
|
|
1313
|
+
await client.query(`
|
|
1314
|
+
UPDATE queen.partition_consumers
|
|
1315
|
+
SET message_batch = $1::jsonb,
|
|
1316
|
+
batch_size = $2::integer,
|
|
1317
|
+
acked_count = 0
|
|
1318
|
+
WHERE partition_id = $3 AND consumer_group = $4
|
|
1319
|
+
`, [JSON.stringify(messageIds), messageIds.length, acquiredPartition.partition_id, actualConsumerGroup]);
|
|
1320
|
+
const leaseUpdateTime = Date.now() - leaseUpdateStart;
|
|
1321
|
+
|
|
1322
|
+
log(`[CURSOR-LEASE] Partition: ${acquiredPartition.partition_name} | Leased ${messageIds.length} messages for processing`);
|
|
1323
|
+
|
|
1324
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1325
|
+
// PHASE 5: Decrypt and Format Messages
|
|
1326
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1327
|
+
|
|
1328
|
+
const decryptStart = Date.now();
|
|
1329
|
+
const formattedMessages = await Promise.all(messages.map(async (msg) => {
|
|
1330
|
+
let payload = msg.payload;
|
|
1331
|
+
|
|
1332
|
+
if (msg.is_encrypted && encryption.isEncryptionEnabled()) {
|
|
1333
|
+
try {
|
|
1334
|
+
payload = await encryption.decryptPayload(msg.payload);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
log(`${LogTypes.ERROR} | Failed to decrypt message ${msg.transaction_id}: ${error.message}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return {
|
|
1341
|
+
id: msg.id,
|
|
1342
|
+
transactionId: msg.transaction_id,
|
|
1343
|
+
traceId: msg.trace_id,
|
|
1344
|
+
queue: msg.queue_name,
|
|
1345
|
+
partition: msg.partition_name,
|
|
1346
|
+
data: payload,
|
|
1347
|
+
payload: payload,
|
|
1348
|
+
retryCount: msg.retry_count || 0,
|
|
1349
|
+
priority: msg.priority || 0,
|
|
1350
|
+
createdAt: msg.created_at,
|
|
1351
|
+
consumerGroup: consumerGroup || null
|
|
1352
|
+
};
|
|
1353
|
+
}));
|
|
1354
|
+
const decryptTime = Date.now() - decryptStart;
|
|
1355
|
+
|
|
1356
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1357
|
+
// PHASE 6: Metrics and Logging
|
|
1358
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1359
|
+
|
|
1360
|
+
const totalTime = Date.now() - startTime;
|
|
1361
|
+
const overhead = totalTime - (connectionWaitTime + candidateQueryTime + messageQueryTime + leaseUpdateTime + decryptTime);
|
|
1362
|
+
const throughput = totalTime > 0 ? (formattedMessages.length / (totalTime / 1000)).toFixed(0) : 0;
|
|
1363
|
+
|
|
1364
|
+
log(`[${LogTypes.POP}] Mode: ${accessMode} | Count: ${formattedMessages.length} | ${throughput} msg/s | ConsumerGroup: ${actualConsumerGroup === '__QUEUE_MODE__' ? 'QUEUE_MODE' : actualConsumerGroup}`);
|
|
1365
|
+
log(` ⏱️ ConnWait:${connectionWaitTime.toFixed(0)}ms CandidateQ:${candidateQueryTime.toFixed(0)}ms MsgQuery:${messageQueryTime.toFixed(0)}ms LeaseUpd:${leaseUpdateTime.toFixed(0)}ms Decrypt:${decryptTime.toFixed(0)}ms Overhead:${overhead.toFixed(0)}ms Total:${totalTime.toFixed(0)}ms`);
|
|
1366
|
+
|
|
1367
|
+
return { messages: formattedMessages };
|
|
1368
|
+
}
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
// Unexpected error
|
|
1371
|
+
if (!candidate.queue_name.startsWith('__')) {
|
|
1372
|
+
log(`LEASE ERROR: Partition=${candidate.partition_name} | Error: ${err.message}`);
|
|
1373
|
+
}
|
|
1374
|
+
throw err;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (acquiredPartition) break;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// If we exhausted all attempts and couldn't get messages
|
|
1382
|
+
if (!acquiredPartition) {
|
|
1383
|
+
log(`WARN: Failed to acquire any partition with messages after ${MAX_ATTEMPTS} attempts`);
|
|
1384
|
+
return { messages: [] };
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1498
1389
|
// Export all functions
|
|
1499
1390
|
return {
|
|
1500
1391
|
pushMessages: pushMessagesBatch,
|
|
1501
1392
|
pushMessagesBatch,
|
|
1502
|
-
popMessages,
|
|
1503
1393
|
acknowledgeMessage,
|
|
1504
1394
|
acknowledgeMessages,
|
|
1505
1395
|
ackMessage: acknowledgeMessage, // Alias for compatibility
|
|
1506
1396
|
reclaimExpiredLeases,
|
|
1507
1397
|
getQueueStats,
|
|
1508
1398
|
getQueueLag,
|
|
1509
|
-
|
|
1399
|
+
uniquePop, // ← New unified function
|
|
1510
1400
|
configureQueue
|
|
1511
1401
|
};
|
|
1512
1402
|
};
|