queen-mq 0.2.0 → 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.
- package/README.md +165 -769
- package/{src → client-js}/benchmark/consumer.js +5 -3
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +272 -6
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/{src → client-js}/test/core-tests.js +7 -2
- package/{src → client-js}/test/edge-case-tests.js +2 -1
- package/client-js/test/human.js +162 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +42 -0
- package/package.json +8 -17
- package/init-db.js +0 -20
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1634
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1633
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/README.md +0 -0
- /package/{src → client-js}/test/advanced-client-tests.js +0 -0
- /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
- /package/{src → client-js}/test/bus-mode-tests.js +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /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
|
-
};
|
package/src/routes/configure.js
DELETED
|
@@ -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
|
-
};
|
package/src/routes/messages.js
DELETED
|
@@ -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
|
-
};
|