queen-mq 0.2.0 → 0.2.22

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 +186 -763
  2. package/{src → client-js}/benchmark/consumer.js +6 -4
  3. package/{src → client-js}/benchmark/producer.js +9 -4
  4. package/{src → client-js}/client/client.js +277 -7
  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/{src → client-js}/test/README.md +30 -9
  9. package/{src → client-js}/test/core-tests.js +7 -2
  10. package/{src → client-js}/test/edge-case-tests.js +2 -1
  11. package/client-js/test/human.js +162 -0
  12. package/client-js/test/partition-transaction-tests.js +482 -0
  13. package/client-js/test/qos0-tests.js +334 -0
  14. package/{src → client-js}/test/test-new.js +55 -0
  15. package/package.json +8 -17
  16. package/init-db.js +0 -20
  17. package/src/cluster-server.js +0 -242
  18. package/src/config.js +0 -229
  19. package/src/database/connection.js +0 -129
  20. package/src/database/poolManager.js +0 -199
  21. package/src/database/schema-v2.sql +0 -278
  22. package/src/managers/eventManager.js +0 -59
  23. package/src/managers/queueManagerOptimized.js +0 -1634
  24. package/src/managers/resourceCache.js +0 -96
  25. package/src/managers/systemEventManager.js +0 -132
  26. package/src/routes/ack.js +0 -26
  27. package/src/routes/configure.js +0 -46
  28. package/src/routes/messages.js +0 -368
  29. package/src/routes/pop.js +0 -69
  30. package/src/routes/push.js +0 -28
  31. package/src/routes/resources.js +0 -330
  32. package/src/routes/status.js +0 -1037
  33. package/src/server.js +0 -1633
  34. package/src/test/MIGRATION_ISSUES.md +0 -174
  35. package/src/test/test.js +0 -4524
  36. package/src/utils/streaming.js +0 -231
  37. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
  38. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
  39. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
  40. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
  41. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
  42. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
  43. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
  44. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
  45. package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
  46. package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
  47. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
  48. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
  49. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
  50. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
  51. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
  52. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
  53. package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
  54. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
  55. package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
  56. package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
  57. package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
  58. package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
  59. package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
  60. package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
  61. package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
  62. package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
  63. package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
  64. package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
  65. package/src/webapp-dist/assets/queen-logo.svg +0 -263
  66. package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
  67. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
  68. package/src/webapp-dist/index.html +0 -15
  69. package/src/websocket/wsServer.js +0 -228
  70. /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
  71. /package/{src → client-js}/benchmark/producer_multi.js +0 -0
  72. /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
  73. /package/{src → client-js}/services/encryptionService.js +0 -0
  74. /package/{src → client-js}/services/evictionService.js +0 -0
  75. /package/{src → client-js}/services/retentionService.js +0 -0
  76. /package/{src → client-js}/services/startupSync.js +0 -0
  77. /package/{src → client-js}/test/advanced-client-tests.js +0 -0
  78. /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
  79. /package/{src → client-js}/test/bus-mode-tests.js +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,96 +0,0 @@
1
- import { EventTypes } from './systemEventManager.js';
2
-
3
- // In-memory cache for resource existence to avoid repeated DB lookups
4
- export const createResourceCache = () => {
5
- const cache = new Map();
6
- // Make TTL configurable - set to 0 to disable caching in multi-server setups
7
- const TTL = process.env.QUEEN_CACHE_TTL ? parseInt(process.env.QUEEN_CACHE_TTL) : 60000; // Default 1 minute
8
-
9
- const getCacheKey = (queue, partition) => `${queue}:${partition || 'Default'}`;
10
-
11
- const checkResource = (queue, partition) => {
12
- // If TTL is 0, caching is disabled
13
- if (TTL === 0) return null;
14
-
15
- const key = getCacheKey(queue, partition);
16
- const cached = cache.get(key);
17
-
18
- if (cached && Date.now() - cached.timestamp < TTL) {
19
- return cached.data;
20
- }
21
-
22
- return null; // Not in cache
23
- };
24
-
25
- const cacheResource = (queue, partition, data) => {
26
- // If TTL is 0, caching is disabled
27
- if (TTL === 0) return;
28
-
29
- const key = getCacheKey(queue, partition);
30
- cache.set(key, { data, timestamp: Date.now() });
31
- };
32
-
33
- const invalidate = (queue, partition) => {
34
- if (queue && partition) {
35
- cache.delete(getCacheKey(queue, partition));
36
- } else if (queue) {
37
- // Clear all partitions for this queue
38
- for (const key of cache.keys()) {
39
- if (key.startsWith(`${queue}:`)) {
40
- cache.delete(key);
41
- }
42
- }
43
- } else {
44
- cache.clear(); // Clear all if no specific resource
45
- }
46
- };
47
-
48
- const invalidateQueue = (queue) => {
49
- // Clear all partitions for this queue when queue config changes
50
- for (const key of cache.keys()) {
51
- if (key.startsWith(`${queue}:`)) {
52
- cache.delete(key);
53
- }
54
- }
55
- };
56
-
57
- // Cleanup old entries periodically (only if caching is enabled)
58
- if (TTL > 0) {
59
- setInterval(() => {
60
- const now = Date.now();
61
- for (const [key, value] of cache.entries()) {
62
- if (now - value.timestamp > TTL) {
63
- cache.delete(key);
64
- }
65
- }
66
- }, Math.min(TTL, 60000)); // Check at most once per minute
67
- }
68
-
69
- // Register event handlers for cache invalidation
70
- const registerEventHandlers = (eventManager) => {
71
- // Invalidate cache on queue changes
72
- eventManager.on(EventTypes.QUEUE_UPDATED, (event) => {
73
- invalidateQueue(event.entityId);
74
- });
75
-
76
- eventManager.on(EventTypes.QUEUE_DELETED, (event) => {
77
- invalidateQueue(event.entityId);
78
- });
79
-
80
- eventManager.on(EventTypes.PARTITION_CREATED, (event) => {
81
- invalidate(event.queueName, event.entityId);
82
- });
83
-
84
- eventManager.on(EventTypes.PARTITION_DELETED, (event) => {
85
- invalidate(event.queueName, event.entityId);
86
- });
87
- };
88
-
89
- return {
90
- checkResource,
91
- cacheResource,
92
- invalidate,
93
- invalidateQueue,
94
- registerEventHandlers
95
- };
96
- };
@@ -1,132 +0,0 @@
1
- import { generateUUID } from '../utils/uuid.js';
2
- import config from '../config.js';
3
-
4
- export const SYSTEM_QUEUE = '__system_events__';
5
-
6
- export const EventTypes = {
7
- // Queue events
8
- QUEUE_CREATED: 'queue.created',
9
- QUEUE_UPDATED: 'queue.updated',
10
- QUEUE_DELETED: 'queue.deleted',
11
-
12
- // Partition events
13
- PARTITION_CREATED: 'partition.created',
14
- PARTITION_DELETED: 'partition.deleted',
15
-
16
- // Future events
17
- CONSUMER_GROUP_CREATED: 'consumer_group.created',
18
- CONSUMER_GROUP_UPDATED: 'consumer_group.updated'
19
- };
20
-
21
- export class SystemEventManager {
22
- constructor(pool, serverInstanceId) {
23
- this.pool = pool;
24
- this.serverInstanceId = serverInstanceId;
25
- this.handlers = new Map();
26
- this.eventQueue = [];
27
- this.batchTimer = null;
28
- this.enabled = config.SYSTEM_EVENTS.ENABLED;
29
- }
30
-
31
- // Register event handlers
32
- on(eventType, handler) {
33
- if (!this.handlers.has(eventType)) {
34
- this.handlers.set(eventType, []);
35
- }
36
- this.handlers.get(eventType).push(handler);
37
- }
38
-
39
- // Emit event locally and to system queue
40
- async emit(eventType, data) {
41
- const event = {
42
- id: generateUUID(),
43
- eventType,
44
- ...data,
45
- timestamp: Date.now(),
46
- sourceServer: this.serverInstanceId,
47
- version: 1
48
- };
49
-
50
- // Handle locally first (immediate consistency for originating server)
51
- await this.handleEventLocally(event);
52
-
53
- // Only publish to system queue if system events are enabled
54
- if (this.enabled) {
55
- // Queue for batch publishing
56
- this.eventQueue.push(event);
57
- this.scheduleBatch();
58
- }
59
- }
60
-
61
- // Process incoming event from system queue
62
- async processSystemEvent(event) {
63
- // Skip own events (already processed locally)
64
- if (event.sourceServer === this.serverInstanceId) {
65
- return;
66
- }
67
-
68
- await this.handleEventLocally(event);
69
- }
70
-
71
- async handleEventLocally(event) {
72
- const handlers = this.handlers.get(event.eventType) || [];
73
- for (const handler of handlers) {
74
- try {
75
- await handler(event);
76
- } catch (error) {
77
- console.error(`Handler failed for ${event.eventType}:`, error);
78
- }
79
- }
80
- }
81
-
82
- scheduleBatch() {
83
- if (this.batchTimer) return;
84
-
85
- this.batchTimer = setTimeout(() => {
86
- this.publishBatch();
87
- }, 10); // 10ms batching
88
- }
89
-
90
- async publishBatch() {
91
- if (this.eventQueue.length === 0) return;
92
-
93
- const events = [...this.eventQueue];
94
- this.eventQueue = [];
95
- this.batchTimer = null;
96
-
97
- try {
98
- // Direct database insert to avoid circular dependency
99
- const client = await this.pool.connect();
100
- try {
101
- for (const event of events) {
102
- await this.insertSystemEvent(client, event);
103
- }
104
- } finally {
105
- client.release();
106
- }
107
- } catch (error) {
108
- console.error('Failed to publish system events:', error);
109
- }
110
- }
111
-
112
- async insertSystemEvent(client, event) {
113
- // Direct insert bypassing queue manager to avoid circular dependency
114
- await client.query(`
115
- INSERT INTO queen.messages (
116
- id, transaction_id, partition_id, payload,
117
- created_at, trace_id
118
- )
119
- SELECT
120
- $1, $2, p.id, $3, NOW(), $4
121
- FROM queen.partitions p
122
- JOIN queen.queues q ON p.queue_id = q.id
123
- WHERE q.name = $5 AND p.name = 'Default'
124
- `, [
125
- generateUUID(),
126
- generateUUID(),
127
- JSON.stringify(event),
128
- event.id,
129
- SYSTEM_QUEUE
130
- ]);
131
- }
132
- }
package/src/routes/ack.js DELETED
@@ -1,26 +0,0 @@
1
- export const createAckRoute = (queueManager) => {
2
- return async (body) => {
3
- const { transactionId, status, error, consumerGroup, leaseId } = body;
4
-
5
- if (!transactionId) {
6
- throw new Error('transactionId is required');
7
- }
8
-
9
- if (!status || !['completed', 'failed'].includes(status)) {
10
- throw new Error('status must be "completed" or "failed"');
11
- }
12
-
13
- const result = await queueManager.acknowledgeMessage(transactionId, status, error, consumerGroup, leaseId);
14
-
15
- if (!result) {
16
- throw new Error('Message not found or not in processing state');
17
- }
18
-
19
- return {
20
- transactionId: result.transactionId || result.transaction_id,
21
- status: result.status,
22
- consumerGroup: consumerGroup || null,
23
- acknowledgedAt: new Date().toISOString()
24
- };
25
- };
26
- };
@@ -1,46 +0,0 @@
1
- export const createConfigureRoute = (queueManager) => {
2
- return async (body) => {
3
- const { queue, partition, namespace, task, options = {} } = body;
4
-
5
- if (!queue) {
6
- throw new Error('queue is required');
7
- }
8
-
9
- // Note: partition parameter is now ignored but still accepted for backward compatibility
10
- // All configuration is at queue level now
11
-
12
- const validOptions = {
13
- leaseTime: options.leaseTime || 300,
14
- maxSize: options.maxSize || 10000,
15
- ttl: options.ttl || 3600,
16
- retryLimit: options.retryLimit || 3,
17
- retryDelay: options.retryDelay || 1000,
18
- deadLetterQueue: options.deadLetterQueue || false,
19
- dlqAfterMaxRetries: options.dlqAfterMaxRetries || false,
20
- priority: options.priority || 0,
21
- delayedProcessing: options.delayedProcessing || 0,
22
- windowBuffer: options.windowBuffer || 0,
23
- retentionSeconds: options.retentionSeconds || 0,
24
- completedRetentionSeconds: options.completedRetentionSeconds || 0,
25
- retentionEnabled: options.retentionEnabled || false,
26
- encryptionEnabled: options.encryptionEnabled,
27
- maxWaitTimeSeconds: options.maxWaitTimeSeconds
28
- };
29
-
30
- // Call the new queue-only configuration method
31
- const result = await queueManager.configureQueue(queue, validOptions, namespace, task);
32
-
33
- return {
34
- queue,
35
- namespace: result.namespace,
36
- task: result.task,
37
- configured: true,
38
- options: result.options,
39
- // Include partition in response for backward compatibility but mark as deprecated
40
- ...(partition && {
41
- partition,
42
- _deprecation_notice: 'Partition-level configuration is deprecated. All configuration is now at queue level.'
43
- })
44
- };
45
- };
46
- };
@@ -1,368 +0,0 @@
1
- import { log, LogTypes } from '../utils/logger.js';
2
- import config from '../config.js';
3
-
4
- export const createMessagesRoutes = (pool, queueManager) => {
5
-
6
- // List messages with filters
7
- const listMessages = async (filters = {}) => {
8
- const { queue, partition, namespace, task, status, limit = config.API.DEFAULT_LIMIT, offset = config.API.DEFAULT_OFFSET } = filters;
9
-
10
- let query = `
11
- SELECT
12
- m.id,
13
- m.transaction_id,
14
- m.payload,
15
- m.created_at,
16
- m.trace_id,
17
- q.name || '/' || p.name as queue_path,
18
- q.name as queue_name,
19
- p.name as partition_name,
20
- q.namespace,
21
- q.task,
22
- q.priority as queue_priority,
23
- pc.lease_expires_at,
24
- pc.last_consumed_created_at,
25
- pc.last_consumed_id,
26
- CASE
27
- WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
28
- WHEN pc.last_consumed_created_at IS NOT NULL AND (
29
- m.created_at < pc.last_consumed_created_at OR
30
- (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', pc.last_consumed_created_at) AND m.id <= pc.last_consumed_id)
31
- ) THEN 'completed'
32
- WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
33
- ELSE 'pending'
34
- END as message_status
35
- FROM queen.messages m
36
- JOIN queen.partitions p ON p.id = m.partition_id
37
- JOIN queen.queues q ON q.id = p.queue_id
38
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
39
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
40
- WHERE 1=1
41
- `;
42
-
43
- const params = [];
44
- let paramCount = 0;
45
-
46
- if (queue) {
47
- params.push(queue);
48
- query += ` AND q.name = $${++paramCount}`;
49
- }
50
- if (partition) {
51
- params.push(partition);
52
- query += ` AND p.name = $${++paramCount}`;
53
- }
54
- if (namespace) {
55
- params.push(namespace);
56
- query += ` AND q.namespace = $${++paramCount}`;
57
- }
58
- if (task) {
59
- params.push(task);
60
- query += ` AND q.task = $${++paramCount}`;
61
- }
62
- if (status) {
63
- // Filter by computed status
64
- params.push(status);
65
- query += ` AND CASE
66
- WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
67
- WHEN pc.last_consumed_created_at IS NOT NULL AND (
68
- m.created_at < pc.last_consumed_created_at OR
69
- (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', pc.last_consumed_created_at) AND m.id <= pc.last_consumed_id)
70
- ) THEN 'completed'
71
- WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
72
- ELSE 'pending'
73
- END = $${++paramCount}`;
74
- }
75
-
76
- query += ` ORDER BY m.created_at DESC, m.id DESC`;
77
-
78
- params.push(limit);
79
- query += ` LIMIT $${++paramCount}`;
80
-
81
- params.push(offset);
82
- query += ` OFFSET $${++paramCount}`;
83
-
84
- const result = await pool.query(query, params);
85
-
86
- return result.rows.map(row => ({
87
- id: row.id,
88
- transactionId: row.transaction_id,
89
- queuePath: row.queue_path,
90
- queue: row.queue_name,
91
- partition: row.partition_name,
92
- namespace: row.namespace,
93
- task: row.task,
94
- payload: row.payload,
95
- status: row.message_status,
96
- traceId: row.trace_id,
97
- queuePriority: row.queue_priority,
98
- createdAt: row.created_at,
99
- leaseExpiresAt: row.lease_expires_at
100
- }));
101
- };
102
-
103
- // Get single message by transaction ID
104
- const getMessage = async (transactionId) => {
105
- const result = await pool.query(`
106
- SELECT
107
- m.id,
108
- m.transaction_id,
109
- m.partition_id,
110
- m.payload,
111
- m.created_at,
112
- m.trace_id,
113
- q.name || '/' || p.name as queue_path,
114
- q.name as queue_name,
115
- p.name as partition_name,
116
- q.namespace,
117
- q.task,
118
- q.lease_time,
119
- q.retry_limit,
120
- q.retry_delay,
121
- q.ttl,
122
- q.priority as queue_priority,
123
- pc.lease_expires_at,
124
- pc.last_consumed_created_at,
125
- pc.last_consumed_id,
126
- dlq.error_message,
127
- dlq.retry_count,
128
- CASE
129
- WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
130
- WHEN pc.last_consumed_created_at IS NOT NULL AND (
131
- m.created_at < pc.last_consumed_created_at OR
132
- (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', pc.last_consumed_created_at) AND m.id <= pc.last_consumed_id)
133
- ) THEN 'completed'
134
- WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
135
- ELSE 'pending'
136
- END as message_status
137
- FROM queen.messages m
138
- JOIN queen.partitions p ON p.id = m.partition_id
139
- JOIN queen.queues q ON q.id = p.queue_id
140
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
141
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
142
- WHERE m.transaction_id = $1
143
- `, [transactionId]);
144
-
145
- if (result.rows.length === 0) {
146
- throw new Error('Message not found');
147
- }
148
-
149
- const row = result.rows[0];
150
- return {
151
- id: row.id,
152
- transactionId: row.transaction_id,
153
- queuePath: row.queue_path,
154
- queue: row.queue_name,
155
- partition: row.partition_name,
156
- namespace: row.namespace,
157
- task: row.task,
158
- payload: row.payload,
159
- status: row.message_status,
160
- traceId: row.trace_id,
161
- createdAt: row.created_at,
162
- errorMessage: row.error_message,
163
- retryCount: row.retry_count,
164
- leaseExpiresAt: row.lease_expires_at,
165
- queueConfig: {
166
- leaseTime: row.lease_time,
167
- retryLimit: row.retry_limit,
168
- retryDelay: row.retry_delay,
169
- ttl: row.ttl,
170
- priority: row.queue_priority
171
- }
172
- };
173
- };
174
-
175
- // Delete message
176
- const deleteMessage = async (transactionId) => {
177
- const result = await pool.query(`
178
- DELETE FROM queen.messages
179
- WHERE transaction_id = $1
180
- RETURNING id
181
- `, [transactionId]);
182
-
183
- if (result.rows.length === 0) {
184
- throw new Error('Message not found');
185
- }
186
-
187
- log(`${LogTypes.DELETE} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
188
- return { deleted: true, transactionId };
189
- };
190
-
191
- // Retry message (remove from DLQ - cursor will process it if not consumed yet)
192
- const retryMessage = async (transactionId) => {
193
- // Find the message
194
- const msgCheck = await pool.query(`
195
- SELECT m.id, m.partition_id, m.created_at,
196
- dlq.consumer_group, dlq.retry_count
197
- FROM queen.messages m
198
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
199
- WHERE m.transaction_id = $1
200
- `, [transactionId]);
201
-
202
- if (msgCheck.rows.length === 0) {
203
- throw new Error('Message not found');
204
- }
205
-
206
- const msg = msgCheck.rows[0];
207
-
208
- if (!msg.consumer_group) {
209
- throw new Error('Message is not in DLQ, cannot retry');
210
- }
211
-
212
- const consumerGroup = msg.consumer_group;
213
- const newRetryCount = (msg.retry_count || 0) + 1;
214
-
215
- // Remove from DLQ - message will be available for consumption if before cursor
216
- const deleteResult = await pool.query(`
217
- DELETE FROM queen.dead_letter_queue
218
- WHERE message_id = $1 AND consumer_group = $2
219
- RETURNING message_id
220
- `, [msg.id, consumerGroup]);
221
-
222
- if (deleteResult.rows.length === 0) {
223
- throw new Error('Message not found in DLQ');
224
- }
225
-
226
- log(`${LogTypes.RETRY} | TransactionId: ${transactionId} | ConsumerGroup: ${consumerGroup}`);
227
- return { retried: true, transactionId, consumerGroup };
228
- };
229
-
230
- // Move message to DLQ
231
- const moveToDLQ = async (transactionId, consumerGroup = '__QUEUE_MODE__', errorMessage = 'Manually moved to DLQ') => {
232
- // Find the message
233
- const msgCheck = await pool.query(`
234
- SELECT m.id, m.partition_id, m.created_at
235
- FROM queen.messages m
236
- WHERE m.transaction_id = $1
237
- `, [transactionId]);
238
-
239
- if (msgCheck.rows.length === 0) {
240
- throw new Error('Message not found');
241
- }
242
-
243
- const msg = msgCheck.rows[0];
244
-
245
- // Insert into dead_letter_queue
246
- const result = await pool.query(`
247
- INSERT INTO queen.dead_letter_queue (message_id, partition_id, consumer_group, error_message, retry_count, original_created_at)
248
- VALUES ($1, $2, $3, $4, 0, $5)
249
- ON CONFLICT DO NOTHING
250
- RETURNING id
251
- `, [msg.id, msg.partition_id, consumerGroup, errorMessage, msg.created_at]);
252
-
253
- if (result.rows.length === 0) {
254
- throw new Error('Message already in DLQ for this consumer group');
255
- }
256
-
257
- log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${msg.id} | ConsumerGroup: ${consumerGroup}`);
258
- return { movedToDLQ: true, transactionId, messageId: msg.id };
259
- };
260
-
261
- // Get related messages (same partition, near in time)
262
- const getRelatedMessages = async (transactionId) => {
263
- // First get the message details
264
- const msgResult = await pool.query(`
265
- SELECT partition_id, created_at
266
- FROM queen.messages
267
- WHERE transaction_id = $1
268
- `, [transactionId]);
269
-
270
- if (msgResult.rows.length === 0) {
271
- return [];
272
- }
273
-
274
- const { partition_id, created_at } = msgResult.rows[0];
275
-
276
- // Get messages from same partition within 1 hour
277
- const result = await pool.query(`
278
- SELECT
279
- m.transaction_id,
280
- m.created_at,
281
- m.payload,
282
- pc.lease_expires_at,
283
- pc.last_consumed_created_at,
284
- pc.last_consumed_id,
285
- CASE
286
- WHEN dlq.message_id IS NOT NULL THEN 'dead_letter'
287
- WHEN pc.last_consumed_created_at IS NOT NULL AND (
288
- m.created_at < pc.last_consumed_created_at OR
289
- (DATE_TRUNC('milliseconds', m.created_at) = DATE_TRUNC('milliseconds', pc.last_consumed_created_at) AND m.id <= pc.last_consumed_id)
290
- ) THEN 'completed'
291
- WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW() THEN 'processing'
292
- ELSE 'pending'
293
- END as message_status
294
- FROM queen.messages m
295
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id AND pc.consumer_group = '__QUEUE_MODE__'
296
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id
297
- WHERE m.partition_id = $1
298
- AND m.transaction_id != $2
299
- AND m.created_at BETWEEN $3::timestamp - INTERVAL '1 hour'
300
- AND $3::timestamp + INTERVAL '1 hour'
301
- ORDER BY ABS(EXTRACT(EPOCH FROM (m.created_at - $3::timestamp)))
302
- LIMIT ${config.ANALYTICS.MAX_RELATED_MESSAGES}
303
- `, [partition_id, transactionId, created_at]);
304
-
305
- return result.rows.map(row => ({
306
- transactionId: row.transaction_id,
307
- status: row.message_status,
308
- createdAt: row.created_at,
309
- payload: row.payload
310
- }));
311
- };
312
-
313
- // Clear all messages in a queue or partition
314
- const clearQueue = async (queue, partition = null) => {
315
- let query;
316
- let params;
317
-
318
- if (partition) {
319
- // Clear specific partition
320
- query = `
321
- DELETE FROM queen.messages
322
- WHERE partition_id IN (
323
- SELECT p.id
324
- FROM queen.partitions p
325
- JOIN queen.queues q ON q.id = p.queue_id
326
- WHERE q.name = $1 AND p.name = $2
327
- )
328
- RETURNING id
329
- `;
330
- params = [queue, partition];
331
- } else {
332
- // Clear entire queue (all partitions)
333
- query = `
334
- DELETE FROM queen.messages
335
- WHERE partition_id IN (
336
- SELECT p.id
337
- FROM queen.partitions p
338
- JOIN queen.queues q ON q.id = p.queue_id
339
- WHERE q.name = $1
340
- )
341
- RETURNING id
342
- `;
343
- params = [queue];
344
- }
345
-
346
- const result = await pool.query(query, params);
347
-
348
- const clearedCount = result.rowCount || 0;
349
- log(`${LogTypes.CLEAR_QUEUE} | Queue: ${queue} | Partition: ${partition || 'all'} | Count: ${clearedCount} messages deleted`);
350
-
351
- return {
352
- cleared: true,
353
- count: clearedCount,
354
- queue,
355
- partition: partition || 'all'
356
- };
357
- };
358
-
359
- return {
360
- listMessages,
361
- getMessage,
362
- deleteMessage,
363
- retryMessage,
364
- moveToDLQ,
365
- getRelatedMessages,
366
- clearQueue
367
- };
368
- };