queen-mq 0.1.6 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +166 -2078
  2. package/{src → client-js}/benchmark/consumer.js +5 -3
  3. package/{src → client-js}/benchmark/producer.js +9 -4
  4. package/client-js/client/client.js +1511 -0
  5. package/{src → client-js}/client/index.js +1 -4
  6. package/{src → client-js}/client/utils/http.js +3 -2
  7. package/{src → client-js}/client/utils/retry.js +7 -1
  8. package/client-js/test/advanced-client-tests.js +761 -0
  9. package/{src → client-js}/test/advanced-pattern-tests.js +2 -2
  10. package/{src → client-js}/test/bus-mode-tests.js +6 -6
  11. package/{src → client-js}/test/core-tests.js +8 -3
  12. package/{src → client-js}/test/edge-case-tests.js +4 -3
  13. package/client-js/test/human.js +162 -0
  14. package/client-js/test/qos0-tests.js +334 -0
  15. package/{src → client-js}/test/test-new.js +78 -1
  16. package/package.json +8 -29
  17. package/init-db.js +0 -20
  18. package/src/client/client.js +0 -586
  19. package/src/cluster-server.js +0 -242
  20. package/src/config.js +0 -229
  21. package/src/database/connection.js +0 -129
  22. package/src/database/poolManager.js +0 -199
  23. package/src/database/schema-v2.sql +0 -278
  24. package/src/managers/eventManager.js +0 -59
  25. package/src/managers/queueManagerOptimized.js +0 -1431
  26. package/src/managers/resourceCache.js +0 -96
  27. package/src/managers/systemEventManager.js +0 -132
  28. package/src/routes/ack.js +0 -26
  29. package/src/routes/configure.js +0 -46
  30. package/src/routes/messages.js +0 -368
  31. package/src/routes/pop.js +0 -69
  32. package/src/routes/push.js +0 -28
  33. package/src/routes/resources.js +0 -330
  34. package/src/routes/status.js +0 -1037
  35. package/src/server.js +0 -1528
  36. package/src/test/MIGRATION_ISSUES.md +0 -174
  37. package/src/test/test.js +0 -4524
  38. package/src/utils/streaming.js +0 -231
  39. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
  40. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
  41. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
  42. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
  43. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
  44. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
  45. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
  46. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
  47. package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
  48. package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
  49. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
  50. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
  51. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
  52. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
  53. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
  54. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
  55. package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
  56. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
  57. package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
  58. package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
  59. package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
  60. package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
  61. package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
  62. package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
  63. package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
  64. package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
  65. package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
  66. package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
  67. package/src/webapp-dist/assets/queen-logo.svg +0 -263
  68. package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
  69. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
  70. package/src/webapp-dist/index.html +0 -15
  71. package/src/websocket/wsServer.js +0 -228
  72. /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
  73. /package/{src → client-js}/benchmark/producer_multi.js +0 -0
  74. /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
  75. /package/{src → client-js}/services/encryptionService.js +0 -0
  76. /package/{src → client-js}/services/evictionService.js +0 -0
  77. /package/{src → client-js}/services/retentionService.js +0 -0
  78. /package/{src → client-js}/services/startupSync.js +0 -0
  79. /package/{src → client-js}/test/README.md +0 -0
  80. /package/{src → client-js}/test/enterprise-tests.js +0 -0
  81. /package/{src → client-js}/test/partition-locking-tests.js +0 -0
  82. /package/{src → client-js}/test/utils.js +0 -0
  83. /package/{src → client-js}/test/window-buffer-test.js +0 -0
  84. /package/{src → client-js}/utils/logger.js +0 -0
  85. /package/{src → client-js}/utils/uuid.js +0 -0
@@ -1,1431 +0,0 @@
1
- import { generateUUID } from '../utils/uuid.js';
2
- import { withTransaction } from '../database/connection.js';
3
- import * as encryption from '../services/encryptionService.js';
4
- import { log, LogTypes } from '../utils/logger.js';
5
- import config from '../config.js';
6
- import { EventTypes } from './systemEventManager.js';
7
-
8
- export const createOptimizedQueueManager = (pool, resourceCache, eventManager, systemEventManager = null) => {
9
-
10
- // Batch size for database operations
11
- const BATCH_INSERT_SIZE = config.QUEUE.BATCH_INSERT_SIZE;
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
-
90
- // Ensure queue exists and partition exist (with caching)
91
- // Queue must be created via configure endpoint, but partitions can be created on-demand
92
- const ensureResources = async (client, queueName, partitionName = 'Default', namespace = null, task = null) => {
93
- // CRITICAL: System queues bypass cache completely
94
- if (queueName.startsWith('__system_')) {
95
- // Direct database query for system queues
96
- const result = await client.query(`
97
- SELECT
98
- q.id as queue_id,
99
- q.name as queue_name,
100
- p.id as partition_id,
101
- q.*
102
- FROM queen.queues q
103
- JOIN queen.partitions p ON p.queue_id = q.id
104
- WHERE q.name = $1 AND p.name = $2
105
- `, [queueName, partitionName]);
106
-
107
- if (result.rows.length === 0) {
108
- throw new Error(`System queue ${queueName} not found`);
109
- }
110
-
111
- const row = result.rows[0];
112
- return {
113
- queueId: row.queue_id,
114
- queueName: row.queue_name,
115
- partitionId: row.partition_id,
116
- queueConfig: {
117
- leaseTime: row.lease_time,
118
- retryLimit: row.retry_limit,
119
- retryDelay: row.retry_delay,
120
- maxSize: row.max_size,
121
- ttl: row.ttl,
122
- deadLetterQueue: row.dead_letter_queue,
123
- dlqAfterMaxRetries: row.dlq_after_max_retries,
124
- delayedProcessing: row.delayed_processing,
125
- windowBuffer: row.window_buffer,
126
- retentionSeconds: row.retention_seconds,
127
- completedRetentionSeconds: row.completed_retention_seconds,
128
- retentionEnabled: row.retention_enabled,
129
- priority: row.priority,
130
- maxQueueSize: row.max_queue_size
131
- },
132
- encryptionEnabled: false, // System queues never encrypted
133
- maxWaitTimeSeconds: row.max_wait_time_seconds
134
- };
135
- }
136
-
137
- // Check cache first, but skip cache if we're updating namespace/task
138
- const cacheKey = `${queueName}:${partitionName}`;
139
- const cached = resourceCache.checkResource(queueName, partitionName);
140
- if (cached && namespace === null && task === null) return cached;
141
-
142
- // Get queue - don't create if it doesn't exist
143
- const queueResult = await client.query(
144
- `SELECT id, name, namespace, task, encryption_enabled, max_wait_time_seconds,
145
- lease_time, retry_limit, retry_delay, max_size, ttl, dead_letter_queue,
146
- dlq_after_max_retries, delayed_processing, window_buffer, retention_seconds,
147
- completed_retention_seconds, retention_enabled, priority, max_queue_size
148
- FROM queen.queues
149
- WHERE name = $1`,
150
- [queueName]
151
- );
152
-
153
- if (queueResult.rows.length === 0) {
154
- // Queue doesn't exist - throw error instead of creating
155
- const errorMsg = `Queue '${queueName}' does not exist. Please create it using the configure endpoint first.`;
156
- log(`${LogTypes.ERROR} | ${errorMsg}`);
157
- throw new Error(errorMsg);
158
- }
159
-
160
- const queue = queueResult.rows[0];
161
- const queueId = queue.id;
162
-
163
- // Insert or get partition (no more options column)
164
- const partitionResult = await client.query(
165
- `INSERT INTO queen.partitions (queue_id, name) VALUES ($1, $2)
166
- ON CONFLICT (queue_id, name) DO UPDATE SET name = EXCLUDED.name
167
- RETURNING id`,
168
- [queueId, partitionName]
169
- );
170
-
171
- const result = {
172
- queueId,
173
- queueName: queue.name,
174
- namespace: queue.namespace,
175
- task: queue.task,
176
- partitionId: partitionResult.rows[0].id,
177
- // All configuration now comes from queue level
178
- queueConfig: {
179
- leaseTime: queue.lease_time,
180
- retryLimit: queue.retry_limit,
181
- retryDelay: queue.retry_delay,
182
- maxSize: queue.max_size,
183
- ttl: queue.ttl,
184
- deadLetterQueue: queue.dead_letter_queue,
185
- dlqAfterMaxRetries: queue.dlq_after_max_retries,
186
- delayedProcessing: queue.delayed_processing,
187
- windowBuffer: queue.window_buffer,
188
- retentionSeconds: queue.retention_seconds,
189
- completedRetentionSeconds: queue.completed_retention_seconds,
190
- retentionEnabled: queue.retention_enabled,
191
- priority: queue.priority,
192
- maxQueueSize: queue.max_queue_size
193
- },
194
- encryptionEnabled: queue.encryption_enabled,
195
- maxWaitTimeSeconds: queue.max_wait_time_seconds
196
- };
197
-
198
- // Cache the result
199
- resourceCache.cacheResource(queueName, partitionName, result);
200
-
201
- return result;
202
- };
203
-
204
- // Optimized batch insert for high throughput (simplified - no status)
205
- const pushMessagesBatch = async (items) => {
206
- const pushStartTime = Date.now();
207
-
208
- // Group items by queue and partition for efficient resource lookup
209
- const partitionGroups = {};
210
- for (const item of items) {
211
- const partition = item.partition ?? 'Default';
212
- const key = `${item.queue}:${partition}`;
213
- if (!partitionGroups[key]) {
214
- partitionGroups[key] = [];
215
- }
216
- partitionGroups[key].push(item);
217
- }
218
-
219
- const allResults = [];
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
-
260
- // Process each partition group
261
- for (const [partitionKey, partitionItems] of Object.entries(partitionGroups)) {
262
- const [queueName, partitionName] = partitionKey.split(':');
263
-
264
- // Use transaction for better error handling with retry on stale cache
265
- let retryCount = 0;
266
- const maxRetries = 1;
267
-
268
- while (retryCount <= maxRetries) {
269
- try {
270
- const results = await withTransaction(pool, async (client) => {
271
- // Ensure resources exist (cached after first call)
272
- const resources = await ensureResources(client, queueName, partitionName);
273
- const { partitionId, encryptionEnabled, queueConfig } = resources;
274
-
275
- // Process items in batches
276
- const batchResults = [];
277
-
278
- for (let i = 0; i < partitionItems.length; i += BATCH_INSERT_SIZE) {
279
- const batch = partitionItems.slice(i, i + BATCH_INSERT_SIZE);
280
-
281
- // Prepare batch data
282
- const messageIds = [];
283
- const transactionIds = [];
284
- const traceIds = [];
285
- const payloads = [];
286
- const encryptedFlags = [];
287
- const duplicates = [];
288
-
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
298
- const dupCheck = await client.query(
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]
304
- );
305
-
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];
313
-
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
- }
322
-
323
- // Handle encryption if enabled
324
- let payload = item.payload;
325
- let isEncrypted = false;
326
-
327
- if (encryptionEnabled && encryption.isEncryptionEnabled()) {
328
- try {
329
- payload = await encryption.encryptPayload(item.payload);
330
- isEncrypted = true;
331
- } catch (error) {
332
- log(`ERROR: Encryption failed for transaction ${transactionId}:`, error);
333
- // Continue with unencrypted payload
334
- }
335
- }
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
-
361
- // Handle large payloads and special cases
362
- let jsonPayload;
363
- try {
364
- // Handle null/undefined payloads - convert to JSON null string
365
- if (result.payload === null || result.payload === undefined) {
366
- jsonPayload = 'null'; // JSONB expects the string 'null' for null values
367
- } else {
368
- // Try to stringify the payload
369
- jsonPayload = JSON.stringify(result.payload);
370
- }
371
- } catch (error) {
372
- // If JSON.stringify fails (e.g., circular references, too large),
373
- // store as a string representation
374
- log(`WARN: Failed to stringify payload for message ${result.messageId}:`, error.message);
375
- jsonPayload = JSON.stringify({
376
- error: 'Payload serialization failed',
377
- type: typeof result.payload,
378
- message: error.message
379
- });
380
- }
381
-
382
- messageIds.push(result.messageId);
383
- transactionIds.push(result.transactionId);
384
- traceIds.push(result.traceId);
385
- payloads.push(jsonPayload);
386
- encryptedFlags.push(result.isEncrypted);
387
- }
388
-
389
- // Batch insert messages with trace_id
390
- if (messageIds.length > 0) {
391
- const insertQuery = `
392
- INSERT INTO queen.messages (id, transaction_id, trace_id, partition_id, payload, is_encrypted)
393
- SELECT * FROM UNNEST($1::uuid[], $2::varchar[], $3::uuid[], $4::uuid[], $5::jsonb[], $6::boolean[])
394
- RETURNING id, transaction_id, trace_id
395
- `;
396
-
397
- let insertResult;
398
- try {
399
- insertResult = await client.query(insertQuery, [
400
- messageIds,
401
- transactionIds,
402
- traceIds,
403
- Array(messageIds.length).fill(partitionId),
404
- payloads,
405
- encryptedFlags
406
- ]);
407
- } catch (error) {
408
- // If foreign key constraint error, the cached partition is stale
409
- if (error.code === '23503' && error.constraint === 'messages_partition_id_fkey') {
410
- // Invalidate cache and throw a more helpful error
411
- resourceCache.invalidate(queueName, partitionName);
412
- throw new Error(`Stale cache detected for queue '${queueName}' partition '${partitionName}'. Please retry the operation.`);
413
- }
414
- throw error;
415
- }
416
-
417
- // Format results
418
- for (const row of insertResult.rows) {
419
- batchResults.push({
420
- id: row.id,
421
- transactionId: row.transaction_id,
422
- traceId: row.trace_id,
423
- status: 'queued'
424
- });
425
- }
426
- }
427
-
428
- // Add duplicates to results
429
- batchResults.push(...duplicates);
430
- }
431
-
432
- // Notify event manager about new messages
433
- const queuePath = `${queueName}/${partitionName}`;
434
- eventManager.notifyMessageAvailable(queuePath);
435
-
436
- return batchResults;
437
- });
438
-
439
- allResults.push(...results);
440
- break; // Success, exit retry loop
441
- } catch (error) {
442
- // Check if it's a stale cache error
443
- if (error.message && error.message.includes('Stale cache detected') && retryCount < maxRetries) {
444
- retryCount++;
445
- // Cache already invalidated in the error handler, just retry
446
- continue;
447
- }
448
- throw error; // Re-throw if not a stale cache error or max retries exceeded
449
- }
450
- }
451
- }
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)`);
455
- return allResults;
456
- };
457
-
458
-
459
- // Acknowledge messages (now per consumer group)
460
- // CURSOR-BASED: Single ACK now routes through batch ACK to advance cursor properly
461
- const acknowledgeMessage = async (transactionId, status = 'completed', error = null, consumerGroup = null) => {
462
- const batchResult = await acknowledgeMessages([{ transactionId, status, error }], consumerGroup);
463
- return batchResult[0] || { status: 'not_found', transaction_id: transactionId };
464
- };
465
-
466
- // Batch acknowledge messages with cursor-based advancement
467
- const acknowledgeMessages = async (acknowledgments, consumerGroup = null) => {
468
- const ackStartTime = Date.now();
469
- const results = [];
470
- const actualConsumerGroup = consumerGroup || '__QUEUE_MODE__';
471
-
472
- // Group by status for efficient processing
473
- const grouped = {
474
- completed: [],
475
- failed: []
476
- };
477
-
478
- acknowledgments.forEach(ack => {
479
- if (grouped[ack.status]) {
480
- grouped[ack.status].push(ack);
481
- }
482
- });
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
-
493
- await withTransaction(pool, async (client) => {
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;
503
-
504
- log(`[${LogTypes.ACK_BATCH}] Total batch failure: ${failedCount} msgs | ${throughput} msg/s | ${ackTime}ms | ConsumerGroup: ${consumerGroup || 'QUEUE_MODE'}`);
505
-
506
- // Just release the lease - next POP will get same batch
507
- const allIds = acknowledgments.map(a => a.transactionId);
508
- await client.query(`
509
- WITH affected_partitions AS (
510
- SELECT DISTINCT m.partition_id
511
- FROM queen.messages m
512
- WHERE m.transaction_id = ANY($1::varchar[])
513
- )
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
520
- FROM affected_partitions ap
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
- }
553
-
554
- const lastMessage = batchInfo.rows[0];
555
- const partitionId = lastMessage.partition_id;
556
- const partitionName = lastMessage.partition_name;
557
-
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]);
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
640
- grouped.completed.forEach(ack => {
641
- results.push({ transactionId: ack.transactionId, status: 'completed' });
642
- });
643
-
644
- grouped.failed.forEach(ack => {
645
- results.push({ transactionId: ack.transactionId, status: 'failed_dlq' });
646
- });
647
- }
648
- });
649
-
650
- return results;
651
- };
652
-
653
- // Reclaim expired leases from partition_consumers
654
- const reclaimExpiredLeases = async () => {
655
- return withTransaction(pool, async (client) => {
656
- // Clear expired leases from partition_consumers
657
- const result = await client.query(
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
666
- AND lease_expires_at < NOW()
667
- RETURNING partition_id, consumer_group`
668
- );
669
-
670
- if (result.rows.length > 0) {
671
- const byGroup = {};
672
- result.rows.forEach(row => {
673
- const group = row.consumer_group || 'QUEUE_MODE';
674
- byGroup[group] = (byGroup[group] || 0) + 1;
675
- });
676
-
677
- Object.entries(byGroup).forEach(([group, count]) => {
678
- log(`${LogTypes.RECLAIM} | Reclaimed ${count} expired leases for consumer group: ${group}`);
679
- });
680
- }
681
-
682
- return result.rows.length;
683
- });
684
- };
685
-
686
- // Get queue statistics (using partition_consumers for fast estimates)
687
- const getQueueStats = async (filters = {}) => {
688
- const { queue, namespace, task, fromDateTime, toDateTime } = filters;
689
-
690
- let query = `
691
- SELECT
692
- q.name as queue,
693
- q.namespace,
694
- q.task,
695
- p.name as partition,
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
712
- FROM queen.queues q
713
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
714
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
715
- AND pc.consumer_group = '__QUEUE_MODE__'
716
- `;
717
-
718
- const conditions = [];
719
- const params = [];
720
-
721
- if (queue) {
722
- conditions.push(`q.name = $${params.length + 1}`);
723
- params.push(queue);
724
- }
725
-
726
- if (namespace) {
727
- conditions.push(`q.namespace = $${params.length + 1}`);
728
- params.push(namespace);
729
- }
730
-
731
- if (task) {
732
- conditions.push(`q.task = $${params.length + 1}`);
733
- params.push(task);
734
- }
735
-
736
- // Add date params for subqueries
737
- if (fromDateTime) {
738
- params.push(fromDateTime);
739
- }
740
-
741
- if (toDateTime) {
742
- params.push(toDateTime);
743
- }
744
-
745
- if (conditions.length > 0) {
746
- query += ` WHERE ${conditions.join(' AND ')}`;
747
- }
748
-
749
- query += ` ORDER BY q.name, p.name`;
750
-
751
- const result = await pool.query(query, params);
752
- return result.rows;
753
- };
754
-
755
- // Get queue lag statistics (simplified with partition_consumers)
756
- const getQueueLag = async (filters = {}) => {
757
- const { queue, namespace, task, fromDateTime, toDateTime } = filters;
758
-
759
- let query = `
760
- WITH lag_stats AS (
761
- SELECT
762
- q.name as queue,
763
- q.namespace,
764
- q.task,
765
- p.name as partition,
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
783
- FROM queen.queues q
784
- LEFT JOIN queen.partitions p ON p.queue_id = q.id
785
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
786
- AND pc.consumer_group = '__QUEUE_MODE__'
787
- `;
788
-
789
- const conditions = [];
790
- const params = [];
791
-
792
- if (queue) {
793
- conditions.push(`q.name = $${params.length + 1}`);
794
- params.push(queue);
795
- }
796
-
797
- if (namespace) {
798
- conditions.push(`q.namespace = $${params.length + 1}`);
799
- params.push(namespace);
800
- }
801
-
802
- if (task) {
803
- conditions.push(`q.task = $${params.length + 1}`);
804
- params.push(task);
805
- }
806
-
807
- if (conditions.length > 0) {
808
- query += ` WHERE ${conditions.join(' AND ')}`;
809
- }
810
-
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
814
- )
815
- SELECT
816
- queue,
817
- namespace,
818
- task,
819
- partition,
820
- jsonb_build_object(
821
- 'pendingCount', pending_count,
822
- 'processingCount', processing_count,
823
- 'totalBacklog', total_backlog,
824
- 'completedMessages', completed_messages,
825
- 'avgProcessingTimeSeconds', avg_processing_time_seconds,
826
- 'medianProcessingTimeSeconds', median_processing_time_seconds,
827
- 'p95ProcessingTimeSeconds', p95_processing_time_seconds,
828
- 'estimatedLagSeconds', CASE
829
- WHEN oldest_unprocessed IS NOT NULL
830
- THEN EXTRACT(EPOCH FROM (NOW() - oldest_unprocessed))
831
- ELSE 0
832
- END,
833
- 'medianLagSeconds', CASE
834
- WHEN oldest_unprocessed IS NOT NULL AND newest_completed IS NOT NULL
835
- THEN EXTRACT(EPOCH FROM (newest_completed - oldest_unprocessed)) / 2
836
- ELSE 0
837
- END,
838
- 'p95LagSeconds', CASE
839
- WHEN oldest_unprocessed IS NOT NULL
840
- THEN EXTRACT(EPOCH FROM (NOW() - oldest_unprocessed)) * 1.5
841
- ELSE 0
842
- END
843
- ) as stats
844
- FROM lag_stats
845
- ORDER BY queue, partition`;
846
-
847
- const result = await pool.query(query, params);
848
- return result.rows;
849
- };
850
-
851
- /**
852
- * Configure queue settings with namespace and task support
853
- * Updates queue configuration options and optionally sets namespace/task
854
- */
855
- const configureQueue = async (queueName, options = {}, namespace = null, task = null) => {
856
- return withTransaction(pool, async (client) => {
857
- // Check if queue exists (for update vs create detection)
858
- const existingQueue = await client.query(
859
- 'SELECT * FROM queen.queues WHERE name = $1',
860
- [queueName]
861
- );
862
- const isUpdate = existingQueue.rows.length > 0;
863
-
864
- // First create/update the queue with namespace and task
865
- const queueResult = await client.query(
866
- `INSERT INTO queen.queues (name, namespace, task) VALUES ($1, $2, $3)
867
- ON CONFLICT (name) DO UPDATE SET
868
- namespace = CASE WHEN EXCLUDED.namespace IS NOT NULL THEN EXCLUDED.namespace ELSE queen.queues.namespace END,
869
- task = CASE WHEN EXCLUDED.task IS NOT NULL THEN EXCLUDED.task ELSE queen.queues.task END
870
- RETURNING id, name, namespace, task`,
871
- [queueName, namespace || null, task || null]
872
- );
873
-
874
- // Build update query for all configuration options
875
- const updates = [];
876
- const params = [];
877
- let paramIndex = 1;
878
-
879
- // Map options to database columns
880
- const optionMappings = {
881
- leaseTime: 'lease_time',
882
- retryLimit: 'retry_limit',
883
- retryDelay: 'retry_delay',
884
- maxSize: 'max_size',
885
- ttl: 'ttl',
886
- deadLetterQueue: 'dead_letter_queue',
887
- dlqAfterMaxRetries: 'dlq_after_max_retries',
888
- delayedProcessing: 'delayed_processing',
889
- windowBuffer: 'window_buffer',
890
- retentionSeconds: 'retention_seconds',
891
- completedRetentionSeconds: 'completed_retention_seconds',
892
- retentionEnabled: 'retention_enabled',
893
- priority: 'priority',
894
- encryptionEnabled: 'encryption_enabled',
895
- maxWaitTimeSeconds: 'max_wait_time_seconds',
896
- maxQueueSize: 'max_queue_size'
897
- };
898
-
899
- // Detect what changed (for update events)
900
- const changes = {};
901
-
902
- // Process each option
903
- for (const [optionKey, columnName] of Object.entries(optionMappings)) {
904
- if (options[optionKey] !== undefined) {
905
- // Track changes for update events
906
- if (isUpdate && existingQueue.rows[0]) {
907
- const old = existingQueue.rows[0];
908
- const newValue = options[optionKey];
909
- if (old[columnName] !== newValue) {
910
- changes[optionKey] = { old: old[columnName], new: newValue };
911
- }
912
- }
913
-
914
- updates.push(`${columnName} = $${paramIndex}`);
915
-
916
- // Handle different data types
917
- if (typeof options[optionKey] === 'boolean') {
918
- params.push(!!options[optionKey]);
919
- } else if (typeof options[optionKey] === 'number' || !isNaN(options[optionKey])) {
920
- params.push(parseInt(options[optionKey]) || 0);
921
- } else {
922
- params.push(options[optionKey]);
923
- }
924
-
925
- paramIndex++;
926
- }
927
- }
928
-
929
- // Apply updates if any
930
- if (updates.length > 0) {
931
- params.push(queueName);
932
- const updateQuery = `UPDATE queen.queues SET ${updates.join(', ')} WHERE name = $${paramIndex}`;
933
- await client.query(updateQuery, params);
934
- }
935
-
936
- // Emit appropriate event (if systemEventManager is available)
937
- if (systemEventManager) {
938
- await systemEventManager.emit(
939
- isUpdate ? EventTypes.QUEUE_UPDATED : EventTypes.QUEUE_CREATED,
940
- {
941
- entityType: 'queue',
942
- entityId: queueName,
943
- changes: isUpdate ? changes : options,
944
- namespace,
945
- task
946
- }
947
- );
948
- }
949
-
950
- // Local cache invalidation (immediate)
951
- resourceCache.invalidateQueue(queueName);
952
-
953
- return {
954
- queue: queueName,
955
- namespace: queueResult.rows[0].namespace,
956
- task: queueResult.rows[0].task,
957
- options
958
- };
959
- });
960
- };
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
- AND (q.window_buffer = 0 OR NOT EXISTS (
1033
- SELECT 1 FROM queen.messages m
1034
- WHERE m.partition_id = p.id
1035
- AND m.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
1036
- ))
1037
- `, [queue, partition]);
1038
-
1039
- if (result.rows.length > 0) {
1040
- candidatePartitions = [result.rows[0]];
1041
- queueInfo = result.rows[0];
1042
- }
1043
-
1044
- } else if (accessMode === 'queue') {
1045
- // CASE 2: Queue-level access - find unlocked partitions WITH unconsumed messages
1046
- const result = await client.query(`
1047
- SELECT
1048
- q.id as queue_id,
1049
- q.name as queue_name,
1050
- q.lease_time,
1051
- q.retry_limit,
1052
- q.delayed_processing,
1053
- q.max_wait_time_seconds,
1054
- q.window_buffer,
1055
- q.priority,
1056
- p.id as partition_id,
1057
- p.name as partition_name
1058
- FROM queen.queues q
1059
- JOIN queen.partitions p ON p.queue_id = q.id
1060
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id
1061
- AND pc.consumer_group = $1
1062
- WHERE q.name = $2
1063
- AND NOT EXISTS (
1064
- SELECT 1 FROM queen.partition_consumers pc2
1065
- WHERE pc2.partition_id = p.id
1066
- AND pc2.consumer_group = $1
1067
- AND pc2.lease_expires_at IS NOT NULL
1068
- AND pc2.lease_expires_at > NOW()
1069
- )
1070
- AND (q.window_buffer = 0 OR NOT EXISTS (
1071
- SELECT 1 FROM queen.messages m
1072
- WHERE m.partition_id = p.id
1073
- AND m.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
1074
- ))
1075
- AND EXISTS (
1076
- SELECT 1 FROM queen.messages m2
1077
- WHERE m2.partition_id = p.id
1078
- AND m2.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)
1079
- AND (q.delayed_processing = 0 OR m2.created_at <= NOW() - INTERVAL '1 second' * q.delayed_processing)
1080
- AND (q.max_wait_time_seconds = 0 OR m2.created_at > NOW() - INTERVAL '1 second' * q.max_wait_time_seconds)
1081
- )
1082
- ORDER BY RANDOM()
1083
- LIMIT $3
1084
- `, [actualConsumerGroup, queue, config.QUEUE.MAX_PARTITION_CANDIDATES]);
1085
-
1086
- if (result.rows.length > 0) {
1087
- candidatePartitions = result.rows;
1088
- queueInfo = result.rows[0];
1089
- }
1090
-
1091
- } else {
1092
- // CASE 3: Filtered access - namespace/task
1093
- // Use CTE to first filter partitions by windowBuffer, then find messages
1094
- let filterQuery = `
1095
- WITH eligible_partitions AS (
1096
- SELECT DISTINCT
1097
- q.id as queue_id,
1098
- q.name as queue_name,
1099
- q.lease_time,
1100
- q.retry_limit,
1101
- q.delayed_processing,
1102
- q.max_wait_time_seconds,
1103
- q.window_buffer,
1104
- q.priority,
1105
- p.id as partition_id,
1106
- p.name as partition_name
1107
- FROM queen.queues q
1108
- JOIN queen.partitions p ON p.queue_id = q.id
1109
- WHERE (q.window_buffer = 0 OR NOT EXISTS (
1110
- SELECT 1 FROM queen.messages m2
1111
- WHERE m2.partition_id = p.id
1112
- AND m2.created_at > NOW() - INTERVAL '1 second' * q.window_buffer
1113
- ))
1114
- AND NOT EXISTS (
1115
- SELECT 1 FROM queen.partition_consumers pc2
1116
- WHERE pc2.partition_id = p.id
1117
- AND pc2.consumer_group = $1
1118
- AND pc2.lease_expires_at IS NOT NULL
1119
- AND pc2.lease_expires_at > NOW()
1120
- )
1121
- )
1122
- SELECT
1123
- ep.*,
1124
- MIN(m.created_at) as oldest_message
1125
- FROM queen.messages m
1126
- JOIN eligible_partitions ep ON m.partition_id = ep.partition_id
1127
- LEFT JOIN queen.partition_consumers pc ON ep.partition_id = pc.partition_id
1128
- AND pc.consumer_group = $1
1129
- WHERE m.id > COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid)
1130
- AND (ep.delayed_processing = 0 OR m.created_at <= NOW() - INTERVAL '1 second' * ep.delayed_processing)
1131
- AND (ep.max_wait_time_seconds = 0 OR
1132
- m.created_at > NOW() - INTERVAL '1 second' * ep.max_wait_time_seconds)
1133
- `;
1134
-
1135
- const params = [actualConsumerGroup];
1136
-
1137
- //Build namespace/task filters for the CTE
1138
- let cteFilters = '';
1139
- if (namespace) {
1140
- params.push(namespace);
1141
- cteFilters += ` AND q.namespace = $${params.length}`;
1142
- }
1143
-
1144
- if (task) {
1145
- params.push(task);
1146
- cteFilters += ` AND q.task = $${params.length}`;
1147
- }
1148
-
1149
- // Insert CTE filters before the closing parenthesis
1150
- filterQuery = filterQuery.replace(' )\n )', ` )${cteFilters}\n )`);
1151
-
1152
- params.push(config.QUEUE.MAX_PARTITION_CANDIDATES);
1153
- filterQuery += `
1154
- GROUP BY ep.queue_id, ep.queue_name, ep.lease_time, ep.retry_limit, ep.delayed_processing,
1155
- ep.max_wait_time_seconds, ep.window_buffer, ep.priority,
1156
- ep.partition_id, ep.partition_name
1157
- HAVING COUNT(*) > 0
1158
- ORDER BY ep.priority DESC, MIN(m.created_at) ASC, RANDOM()
1159
- LIMIT $${params.length}
1160
- `;
1161
-
1162
- const result = await client.query(filterQuery, params);
1163
-
1164
- if (result.rows.length > 0) {
1165
- candidatePartitions = result.rows;
1166
- queueInfo = result.rows[0];
1167
- }
1168
- }
1169
-
1170
- const candidateQueryTime = Date.now() - candidateQueryStart;
1171
-
1172
- if (candidatePartitions.length === 0) {
1173
- return { messages: [] };
1174
- }
1175
-
1176
- // ─────────────────────────────────────────────────────────────────────
1177
- // PHASE 2: Try to Acquire Lease on a Candidate Partition (shared logic)
1178
- // ─────────────────────────────────────────────────────────────────────
1179
-
1180
- let acquiredPartition = null;
1181
- const MAX_ATTEMPTS = accessMode === 'direct' ? 1 : 3;
1182
-
1183
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1184
- // Shuffle candidates to distribute across consumers
1185
- const shuffled = [...candidatePartitions];
1186
- for (let i = shuffled.length - 1; i > 0; i--) {
1187
- const j = Math.floor(Math.random() * (i + 1));
1188
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1189
- }
1190
-
1191
- // Try to acquire lease on each candidate
1192
- for (const candidate of shuffled) {
1193
- const leaseAcqStart = Date.now();
1194
-
1195
- // Try to acquire lease - check if lease is available based on DB state
1196
- // The key insight: released_at IS NULL means someone holds the lease
1197
- // We can only acquire if: released_at IS NOT NULL OR lease expired
1198
- try {
1199
- // Get or create cursor position for this partition/consumer group
1200
- // Handle subscription modes for bus mode (new-only, from-timestamp, etc.)
1201
- let initialCursorId = '00000000-0000-0000-0000-000000000000';
1202
- let initialCursorTimestamp = null;
1203
-
1204
- // For bus mode with subscription preferences
1205
- if (consumerGroup && (subscriptionMode || subscriptionFrom)) {
1206
- if (subscriptionMode === 'new' || subscriptionMode === 'new-only' || subscriptionFrom === 'now') {
1207
- // Start from NOW - get the latest message ID to use as cursor
1208
- const latestMsg = await client.query(`
1209
- SELECT id, created_at FROM queen.messages
1210
- WHERE partition_id = $1
1211
- ORDER BY created_at DESC, id DESC
1212
- LIMIT 1
1213
- `, [candidate.partition_id]);
1214
-
1215
- if (latestMsg.rows.length > 0) {
1216
- initialCursorId = latestMsg.rows[0].id;
1217
- initialCursorTimestamp = latestMsg.rows[0].created_at;
1218
- }
1219
- }
1220
- // else: subscriptionFrom='all' or no preference → start from beginning (00000000...)
1221
- }
1222
-
1223
- // Step 1: Ensure cursor exists (idempotent)
1224
- await client.query(`
1225
- INSERT INTO queen.partition_consumers (
1226
- partition_id,
1227
- consumer_group,
1228
- last_consumed_id,
1229
- last_consumed_created_at
1230
- )
1231
- VALUES ($1, $2, $3, $4)
1232
- ON CONFLICT (partition_id, consumer_group) DO NOTHING
1233
- `, [candidate.partition_id, actualConsumerGroup, initialCursorId, initialCursorTimestamp]);
1234
-
1235
- // Step 2: Try to acquire lease and get cursor position
1236
- const leaseResult = await client.query(`
1237
- UPDATE queen.partition_consumers
1238
- SET lease_expires_at = NOW() + INTERVAL '1 second' * $3,
1239
- lease_acquired_at = NOW(),
1240
- message_batch = NULL,
1241
- batch_size = 0,
1242
- acked_count = 0
1243
- WHERE partition_id = $1
1244
- AND consumer_group = $2
1245
- AND (lease_expires_at IS NULL OR lease_expires_at <= NOW())
1246
- RETURNING partition_id, last_consumed_id, last_consumed_created_at,
1247
- total_messages_consumed, lease_expires_at
1248
- `, [candidate.partition_id, actualConsumerGroup, candidate.lease_time]);
1249
-
1250
- // Check if we acquired the lease
1251
- const acquired = leaseResult.rows.length > 0;
1252
- const result = leaseResult.rows[0];
1253
-
1254
- if (acquired) {
1255
- // Lease acquired! Now check if this partition has messages
1256
- // If not, release and try next candidate
1257
- const messageQueryStart = Date.now();
1258
-
1259
- const cursor = result;
1260
- const cursorId = cursor.last_consumed_id || '00000000-0000-0000-0000-000000000000';
1261
- const totalConsumed = cursor.total_messages_consumed || 0;
1262
-
1263
- // Cursor-based query using ONLY UUID comparison (UUIDv7 is time-ordered)
1264
- // Conditionally add timestamp filters only when configured
1265
- const hasDelayedProcessing = candidate.delayed_processing > 0;
1266
- const hasEviction = candidate.max_wait_time_seconds > 0;
1267
-
1268
- let whereClause = 'm.partition_id = $4 AND m.id > $5::uuid';
1269
- let queryParams = [
1270
- candidate.partition_name,
1271
- candidate.queue_name,
1272
- candidate.priority || 0,
1273
- candidate.partition_id,
1274
- cursorId
1275
- ];
1276
- let paramIndex = 6;
1277
-
1278
- if (hasDelayedProcessing) {
1279
- whereClause += ` AND m.created_at <= NOW() - INTERVAL '1 second' * $${paramIndex}`;
1280
- queryParams.push(candidate.delayed_processing);
1281
- paramIndex++;
1282
- }
1283
-
1284
- if (hasEviction) {
1285
- whereClause += ` AND m.created_at > NOW() - INTERVAL '1 second' * $${paramIndex}`;
1286
- queryParams.push(candidate.max_wait_time_seconds);
1287
- paramIndex++;
1288
- }
1289
-
1290
- queryParams.push(batch);
1291
- const limitParam = `$${paramIndex}`;
1292
-
1293
- const messagesResult = await client.query(`
1294
- SELECT
1295
- m.id,
1296
- m.transaction_id,
1297
- m.trace_id,
1298
- m.payload,
1299
- m.is_encrypted,
1300
- m.created_at,
1301
- $1 as partition_name,
1302
- $2 as queue_name,
1303
- $3 as priority,
1304
- 0 as retry_count
1305
- FROM queen.messages m
1306
- WHERE ${whereClause}
1307
- ORDER BY m.created_at ASC, m.id ASC
1308
- LIMIT ${limitParam}
1309
- FOR UPDATE OF m SKIP LOCKED
1310
- `, queryParams)
1311
- .catch(err => {
1312
- if (err.code === '55P03') { // Lock not available
1313
- return { rows: [] };
1314
- }
1315
- throw err;
1316
- });
1317
-
1318
- const messageQueryTime = Date.now() - messageQueryStart;
1319
- const messages = messagesResult.rows;
1320
-
1321
- // Check if we found messages
1322
- if (messages.length === 0) {
1323
- // No messages in this partition (cursor exhausted) - release lease and try next candidate
1324
- log(`[CURSOR] Partition ${candidate.partition_name} exhausted after ${totalConsumed} messages`);
1325
- await client.query(`
1326
- UPDATE queen.partition_consumers
1327
- SET lease_expires_at = NULL,
1328
- lease_acquired_at = NULL
1329
- WHERE partition_id = $1 AND consumer_group = $2
1330
- `, [candidate.partition_id, actualConsumerGroup]);
1331
- continue; // Try next candidate partition
1332
- }
1333
-
1334
- // Success! We have messages from this partition
1335
- acquiredPartition = candidate;
1336
-
1337
- // ─────────────────────────────────────────────────────────────────────
1338
- // PHASE 4: Update Partition Lease with Message Batch
1339
- // ─────────────────────────────────────────────────────────────────────
1340
-
1341
- const leaseUpdateStart = Date.now();
1342
- const messageIds = messages.map(m => m.id);
1343
- await client.query(`
1344
- UPDATE queen.partition_consumers
1345
- SET message_batch = $1::jsonb,
1346
- batch_size = $2::integer,
1347
- acked_count = 0
1348
- WHERE partition_id = $3 AND consumer_group = $4
1349
- `, [JSON.stringify(messageIds), messageIds.length, acquiredPartition.partition_id, actualConsumerGroup]);
1350
- const leaseUpdateTime = Date.now() - leaseUpdateStart;
1351
-
1352
- log(`[CURSOR-LEASE] Partition: ${acquiredPartition.partition_name} | Leased ${messageIds.length} messages for processing`);
1353
-
1354
- // ─────────────────────────────────────────────────────────────────────
1355
- // PHASE 5: Decrypt and Format Messages
1356
- // ─────────────────────────────────────────────────────────────────────
1357
-
1358
- const decryptStart = Date.now();
1359
- const formattedMessages = await Promise.all(messages.map(async (msg) => {
1360
- let payload = msg.payload;
1361
-
1362
- if (msg.is_encrypted && encryption.isEncryptionEnabled()) {
1363
- try {
1364
- payload = await encryption.decryptPayload(msg.payload);
1365
- } catch (error) {
1366
- log(`${LogTypes.ERROR} | Failed to decrypt message ${msg.transaction_id}: ${error.message}`);
1367
- }
1368
- }
1369
-
1370
- return {
1371
- id: msg.id,
1372
- transactionId: msg.transaction_id,
1373
- traceId: msg.trace_id,
1374
- queue: msg.queue_name,
1375
- partition: msg.partition_name,
1376
- data: payload,
1377
- retryCount: msg.retry_count || 0,
1378
- priority: msg.priority || 0,
1379
- createdAt: msg.created_at,
1380
- consumerGroup: consumerGroup || null
1381
- };
1382
- }));
1383
- const decryptTime = Date.now() - decryptStart;
1384
-
1385
- // ─────────────────────────────────────────────────────────────────────
1386
- // PHASE 6: Metrics and Logging
1387
- // ─────────────────────────────────────────────────────────────────────
1388
-
1389
- const totalTime = Date.now() - startTime;
1390
- const overhead = totalTime - (connectionWaitTime + candidateQueryTime + messageQueryTime + leaseUpdateTime + decryptTime);
1391
- const throughput = totalTime > 0 ? (formattedMessages.length / (totalTime / 1000)).toFixed(0) : 0;
1392
-
1393
- log(`[${LogTypes.POP}] Mode: ${accessMode} | Count: ${formattedMessages.length} | ${throughput} msg/s | ConsumerGroup: ${actualConsumerGroup === '__QUEUE_MODE__' ? 'QUEUE_MODE' : actualConsumerGroup}`);
1394
- 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`);
1395
-
1396
- return { messages: formattedMessages };
1397
- }
1398
- } catch (err) {
1399
- // Unexpected error
1400
- if (!candidate.queue_name.startsWith('__')) {
1401
- log(`LEASE ERROR: Partition=${candidate.partition_name} | Error: ${err.message}`);
1402
- }
1403
- throw err;
1404
- }
1405
- }
1406
-
1407
- if (acquiredPartition) break;
1408
- }
1409
-
1410
- // If we exhausted all attempts and couldn't get messages
1411
- if (!acquiredPartition) {
1412
- log(`WARN: Failed to acquire any partition with messages after ${MAX_ATTEMPTS} attempts`);
1413
- return { messages: [] };
1414
- }
1415
- });
1416
- };
1417
-
1418
- // Export all functions
1419
- return {
1420
- pushMessages: pushMessagesBatch,
1421
- pushMessagesBatch,
1422
- acknowledgeMessage,
1423
- acknowledgeMessages,
1424
- ackMessage: acknowledgeMessage, // Alias for compatibility
1425
- reclaimExpiredLeases,
1426
- getQueueStats,
1427
- getQueueLag,
1428
- uniquePop, // ← New unified function
1429
- configureQueue
1430
- };
1431
- };