queen-mq 0.1.0

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 (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,46 @@
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
+ };
@@ -0,0 +1,298 @@
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.status,
16
+ m.worker_id,
17
+ m.created_at,
18
+ m.locked_at,
19
+ m.completed_at,
20
+ m.failed_at,
21
+ m.error_message,
22
+ m.retry_count,
23
+ m.lease_expires_at,
24
+ q.name || '/' || p.name as queue_path,
25
+ q.name as queue_name,
26
+ p.name as partition_name,
27
+ q.namespace,
28
+ q.task
29
+ FROM queen.messages m
30
+ JOIN queen.partitions p ON p.id = m.partition_id
31
+ JOIN queen.queues q ON q.id = p.queue_id
32
+ WHERE 1=1
33
+ `;
34
+
35
+ const params = [];
36
+ let paramCount = 0;
37
+
38
+ if (queue) {
39
+ params.push(queue);
40
+ query += ` AND q.name = $${++paramCount}`;
41
+ }
42
+ if (partition) {
43
+ params.push(partition);
44
+ query += ` AND p.name = $${++paramCount}`;
45
+ }
46
+ if (namespace) {
47
+ params.push(namespace);
48
+ query += ` AND q.namespace = $${++paramCount}`;
49
+ }
50
+ if (task) {
51
+ params.push(task);
52
+ query += ` AND q.task = $${++paramCount}`;
53
+ }
54
+ if (status) {
55
+ params.push(status);
56
+ query += ` AND m.status = $${++paramCount}`;
57
+ }
58
+
59
+ query += ` ORDER BY m.created_at DESC`;
60
+
61
+ params.push(limit);
62
+ query += ` LIMIT $${++paramCount}`;
63
+
64
+ params.push(offset);
65
+ query += ` OFFSET $${++paramCount}`;
66
+
67
+ const result = await pool.query(query, params);
68
+
69
+ return result.rows.map(row => ({
70
+ id: row.id,
71
+ transactionId: row.transaction_id,
72
+ queuePath: row.queue_path,
73
+ queue: row.queue_name,
74
+ partition: row.partition_name,
75
+ namespace: row.namespace,
76
+ task: row.task,
77
+ payload: row.payload,
78
+ status: row.status,
79
+ workerId: row.worker_id,
80
+ createdAt: row.created_at,
81
+ lockedAt: row.locked_at,
82
+ completedAt: row.completed_at,
83
+ failedAt: row.failed_at,
84
+ errorMessage: row.error_message,
85
+ retryCount: row.retry_count,
86
+ leaseExpiresAt: row.lease_expires_at
87
+ }));
88
+ };
89
+
90
+ // Get single message by transaction ID
91
+ const getMessage = async (transactionId) => {
92
+ const result = await pool.query(`
93
+ SELECT
94
+ m.*,
95
+ q.name || '/' || p.name as queue_path,
96
+ q.name as queue_name,
97
+ p.name as partition_name,
98
+ q.namespace,
99
+ q.task,
100
+ q.lease_time,
101
+ q.retry_limit,
102
+ q.retry_delay,
103
+ q.ttl,
104
+ q.priority
105
+ FROM queen.messages m
106
+ JOIN queen.partitions p ON p.id = m.partition_id
107
+ JOIN queen.queues q ON q.id = p.queue_id
108
+ WHERE m.transaction_id = $1
109
+ `, [transactionId]);
110
+
111
+ if (result.rows.length === 0) {
112
+ throw new Error('Message not found');
113
+ }
114
+
115
+ const row = result.rows[0];
116
+ return {
117
+ id: row.id,
118
+ transactionId: row.transaction_id,
119
+ queuePath: row.queue_path,
120
+ queue: row.queue_name,
121
+ partition: row.partition_name,
122
+ namespace: row.namespace,
123
+ task: row.task,
124
+ payload: row.payload,
125
+ status: row.status,
126
+ workerId: row.worker_id,
127
+ createdAt: row.created_at,
128
+ lockedAt: row.locked_at,
129
+ completedAt: row.completed_at,
130
+ failedAt: row.failed_at,
131
+ errorMessage: row.error_message,
132
+ retryCount: row.retry_count,
133
+ leaseExpiresAt: row.lease_expires_at,
134
+ queueConfig: {
135
+ leaseTime: row.lease_time,
136
+ retryLimit: row.retry_limit,
137
+ retryDelay: row.retry_delay,
138
+ ttl: row.ttl,
139
+ priority: row.priority
140
+ }
141
+ };
142
+ };
143
+
144
+ // Delete message
145
+ const deleteMessage = async (transactionId) => {
146
+ const result = await pool.query(`
147
+ DELETE FROM queen.messages
148
+ WHERE transaction_id = $1
149
+ RETURNING id
150
+ `, [transactionId]);
151
+
152
+ if (result.rows.length === 0) {
153
+ throw new Error('Message not found');
154
+ }
155
+
156
+ log(`${LogTypes.DELETE} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
157
+ return { deleted: true, transactionId };
158
+ };
159
+
160
+ // Retry message
161
+ const retryMessage = async (transactionId) => {
162
+ const result = await pool.query(`
163
+ UPDATE queen.messages
164
+ SET
165
+ status = 'pending',
166
+ worker_id = NULL,
167
+ locked_at = NULL,
168
+ lease_expires_at = NULL,
169
+ retry_count = retry_count + 1,
170
+ error_message = NULL
171
+ WHERE transaction_id = $1
172
+ AND status IN ('failed', 'dead_letter')
173
+ RETURNING id, retry_count
174
+ `, [transactionId]);
175
+
176
+ if (result.rows.length === 0) {
177
+ throw new Error('Message not found or not in failed state');
178
+ }
179
+
180
+ log(`${LogTypes.RETRY} | TransactionId: ${transactionId} | NewRetryCount: ${result.rows[0].retry_count}`);
181
+ return { retried: true, transactionId };
182
+ };
183
+
184
+ // Move message to DLQ
185
+ const moveToDLQ = async (transactionId) => {
186
+ const result = await pool.query(`
187
+ UPDATE queen.messages
188
+ SET
189
+ status = 'dead_letter',
190
+ failed_at = NOW()
191
+ WHERE transaction_id = $1
192
+ AND status = 'failed'
193
+ RETURNING id
194
+ `, [transactionId]);
195
+
196
+ if (result.rows.length === 0) {
197
+ throw new Error('Message not found or not in failed state');
198
+ }
199
+
200
+ log(`${LogTypes.MANUAL_DLQ} | TransactionId: ${transactionId} | MessageId: ${result.rows[0].id}`);
201
+ return { movedToDLQ: true, transactionId };
202
+ };
203
+
204
+ // Get related messages (same partition, near in time)
205
+ const getRelatedMessages = async (transactionId) => {
206
+ // First get the message details
207
+ const msgResult = await pool.query(`
208
+ SELECT partition_id, created_at
209
+ FROM queen.messages
210
+ WHERE transaction_id = $1
211
+ `, [transactionId]);
212
+
213
+ if (msgResult.rows.length === 0) {
214
+ return [];
215
+ }
216
+
217
+ const { partition_id, created_at } = msgResult.rows[0];
218
+
219
+ // Get messages from same partition within 1 hour
220
+ const result = await pool.query(`
221
+ SELECT
222
+ m.transaction_id,
223
+ m.status,
224
+ m.created_at,
225
+ m.payload
226
+ FROM queen.messages m
227
+ WHERE m.partition_id = $1
228
+ AND m.transaction_id != $2
229
+ AND m.created_at BETWEEN $3::timestamp - INTERVAL '1 hour'
230
+ AND $3::timestamp + INTERVAL '1 hour'
231
+ ORDER BY ABS(EXTRACT(EPOCH FROM (m.created_at - $3::timestamp)))
232
+ LIMIT ${config.ANALYTICS.MAX_RELATED_MESSAGES}
233
+ `, [partition_id, transactionId, created_at]);
234
+
235
+ return result.rows.map(row => ({
236
+ transactionId: row.transaction_id,
237
+ status: row.status,
238
+ createdAt: row.created_at,
239
+ payload: row.payload
240
+ }));
241
+ };
242
+
243
+ // Clear all messages in a queue or partition
244
+ const clearQueue = async (queue, partition = null) => {
245
+ let query;
246
+ let params;
247
+
248
+ if (partition) {
249
+ // Clear specific partition
250
+ query = `
251
+ DELETE FROM queen.messages
252
+ WHERE partition_id IN (
253
+ SELECT p.id
254
+ FROM queen.partitions p
255
+ JOIN queen.queues q ON q.id = p.queue_id
256
+ WHERE q.name = $1 AND p.name = $2
257
+ )
258
+ RETURNING id
259
+ `;
260
+ params = [queue, partition];
261
+ } else {
262
+ // Clear entire queue (all partitions)
263
+ query = `
264
+ DELETE FROM queen.messages
265
+ WHERE partition_id IN (
266
+ SELECT p.id
267
+ FROM queen.partitions p
268
+ JOIN queen.queues q ON q.id = p.queue_id
269
+ WHERE q.name = $1
270
+ )
271
+ RETURNING id
272
+ `;
273
+ params = [queue];
274
+ }
275
+
276
+ const result = await pool.query(query, params);
277
+
278
+ const clearedCount = result.rowCount || 0;
279
+ log(`${LogTypes.CLEAR_QUEUE} | Queue: ${queue} | Partition: ${partition || 'all'} | Count: ${clearedCount} messages deleted`);
280
+
281
+ return {
282
+ cleared: true,
283
+ count: clearedCount,
284
+ queue,
285
+ partition: partition || 'all'
286
+ };
287
+ };
288
+
289
+ return {
290
+ listMessages,
291
+ getMessage,
292
+ deleteMessage,
293
+ retryMessage,
294
+ moveToDLQ,
295
+ getRelatedMessages,
296
+ clearQueue
297
+ };
298
+ };
@@ -0,0 +1,85 @@
1
+ import config from '../config.js';
2
+
3
+ export const createPopRoute = (queueManager, eventManager) => {
4
+ return async (scope, options = {}) => {
5
+ const {
6
+ wait = false,
7
+ timeout = config.QUEUE.DEFAULT_TIMEOUT,
8
+ batch = config.QUEUE.DEFAULT_BATCH_SIZE,
9
+ consumerGroup = scope.consumerGroup || null,
10
+ subscriptionMode = scope.subscriptionMode || null,
11
+ subscriptionFrom = scope.subscriptionFrom || null
12
+ } = options;
13
+ const maxTimeout = config.QUEUE.MAX_TIMEOUT;
14
+ const effectiveTimeout = Math.min(timeout, maxTimeout);
15
+
16
+ // Handle different pop scenarios
17
+ if (scope.namespace || scope.task) {
18
+ // Pop with filters (namespace or task)
19
+ let result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false, timeout: effectiveTimeout });
20
+
21
+ if (result.messages.length > 0 || !wait) {
22
+ return result;
23
+ }
24
+
25
+ // Simple polling for filtered pops (no specific queue path to wait on)
26
+ const startTime = Date.now();
27
+ const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL_FILTERED, effectiveTimeout / 10);
28
+
29
+ while (Date.now() - startTime < effectiveTimeout) {
30
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
31
+ result = await queueManager.popMessagesWithFilters(scope, { batch, wait: false });
32
+ if (result.messages.length > 0) {
33
+ return result;
34
+ }
35
+ }
36
+
37
+ return { messages: [] };
38
+ }
39
+
40
+ // Regular queue/partition pop (now with consumer group support)
41
+ const popScope = {
42
+ ...scope,
43
+ consumerGroup: consumerGroup || scope.consumerGroup
44
+ };
45
+
46
+ const popOptions = {
47
+ batch,
48
+ wait: false,
49
+ timeout: effectiveTimeout,
50
+ subscriptionMode,
51
+ subscriptionFrom
52
+ };
53
+
54
+ let result = await queueManager.popMessages(popScope, popOptions);
55
+
56
+ if (result.messages.length > 0 || !wait) {
57
+ return result;
58
+ }
59
+
60
+ // Long polling with event-driven optimization
61
+ const queuePath = scope.partition
62
+ ? `${scope.queue}/${scope.partition}`
63
+ : scope.queue;
64
+
65
+ const startTime = Date.now();
66
+ const pollInterval = Math.min(config.QUEUE.POLL_INTERVAL, effectiveTimeout / 10);
67
+
68
+ while (Date.now() - startTime < effectiveTimeout) {
69
+ // Wait for message notification or short timeout
70
+ const notification = await Promise.race([
71
+ eventManager.waitForMessage(queuePath, pollInterval),
72
+ new Promise(resolve => setTimeout(() => resolve(null), pollInterval))
73
+ ]);
74
+
75
+ // Try to get messages whether notified or not
76
+ result = await queueManager.popMessages(popScope, popOptions);
77
+ if (result.messages.length > 0) {
78
+ return result;
79
+ }
80
+ }
81
+
82
+ // Timeout reached, no messages
83
+ return { messages: [] };
84
+ };
85
+ };
@@ -0,0 +1,28 @@
1
+ export const createPushRoute = (queueManager) => {
2
+ return async (body) => {
3
+ const { items = [], config = {} } = body;
4
+
5
+ if (!items || items.length === 0) {
6
+ throw new Error('No items to push');
7
+ }
8
+
9
+ // Validate items
10
+ for (const item of items) {
11
+ if (!item.queue) {
12
+ throw new Error('Each item must have a queue');
13
+ }
14
+ // Allow missing, null or undefined payloads - they'll be stored as null in JSONB
15
+ // If payload is not provided, set it to null
16
+ if (!('payload' in item)) {
17
+ item.payload = null;
18
+ }
19
+ // partition is optional, defaults to "Default"
20
+ }
21
+
22
+ const results = await queueManager.pushMessages(items);
23
+
24
+ return {
25
+ messages: results
26
+ };
27
+ };
28
+ };